diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
22 files changed, 647 insertions, 362 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 428e62dc6b..d2404e63c5 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -37,8 +37,21 @@ module ActionDispatch end module Response - def cache_control - @cache_control ||= {} + attr_reader :cache_control + + def initialize(*) + status, header, body = super + + @cache_control = {} + @etag = self["ETag"] + + if cache_control = self["Cache-Control"] + cache_control.split(/,\s*/).each do |segment| + first, last = segment.split("=") + last ||= true + @cache_control[first.to_sym] = last + end + end end def last_modified @@ -65,7 +78,7 @@ module ActionDispatch def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) - @etag = %("#{Digest::MD5.hexdigest(key)}") + @etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}") end private @@ -100,6 +113,8 @@ module ActionDispatch def set_conditional_cache_control! control = @cache_control + return if self["Cache-Control"].present? + if control.empty? headers["Cache-Control"] = DEFAULT_CACHE_CONTROL elsif @cache_control[:no_cache] diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 1958e1668d..451b79b190 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -25,9 +25,16 @@ module ActionDispatch module FilterParameters extend ActiveSupport::Concern + mattr_reader :compiled_parameter_filter_for + @@compiled_parameter_filter_for = {} + # Return a hash of parameters with all sensitive data replaced. def filtered_parameters - @filtered_parameters ||= process_parameter_filter(parameters) + @filtered_parameters ||= if filtering_parameters? + process_parameter_filter(parameters) + else + parameters.dup + end end alias :fitered_params :filtered_parameters @@ -46,10 +53,18 @@ module ActionDispatch protected - def compile_parameter_filter #:nodoc: + def filtering_parameters? #:nodoc: + @env["action_dispatch.parameter_filter"].present? + end + + def process_parameter_filter(params) #:nodoc: + compiled_parameter_filter_for(@env["action_dispatch.parameter_filter"]).call(params) + end + + def compile_parameter_filter(filters) #:nodoc: strings, regexps, blocks = [], [], [] - Array(@env["action_dispatch.parameter_filter"]).each do |item| + filters.each do |item| case item when NilClass when Proc @@ -65,34 +80,34 @@ module ActionDispatch [regexps, blocks] end - def filtering_parameters? #:nodoc: - @env["action_dispatch.parameter_filter"].present? - end + def compiled_parameter_filter_for(filters) #:nodoc: + @@compiled_parameter_filter_for[filters] ||= begin + regexps, blocks = compile_parameter_filter(filters) - def process_parameter_filter(original_params) #:nodoc: - return original_params.dup unless filtering_parameters? + lambda do |original_params| + filtered_params = {} - filtered_params = {} - regexps, blocks = compile_parameter_filter + original_params.each do |key, value| + if regexps.find { |r| key =~ r } + value = '[FILTERED]' + elsif value.is_a?(Hash) + value = process_parameter_filter(value) + elsif value.is_a?(Array) + value = value.map { |v| v.is_a?(Hash) ? process_parameter_filter(v) : v } + elsif blocks.present? + key = key.dup + value = value.dup if value.duplicable? + blocks.each { |b| b.call(key, value) } + end - original_params.each do |key, value| - if regexps.find { |r| key =~ r } - value = '[FILTERED]' - elsif value.is_a?(Hash) - value = process_parameter_filter(value) - elsif value.is_a?(Array) - value = value.map { |i| process_parameter_filter(i) } - elsif blocks.present? - key = key.dup - value = value.dup if value.duplicable? - blocks.each { |b| b.call(key, value) } - end + filtered_params[key] = value + end - filtered_params[key] = value + filtered_params + end end - - filtered_params end + end end end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 40617e239a..fec250e928 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -67,21 +67,6 @@ module ActionDispatch @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] end - # Returns a symbolized version of the <tt>:format</tt> parameter of the request. - # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt> - # otherwise. - def template_format - parameter_format = parameters[:format] - - if parameter_format - parameter_format - elsif xhr? - :js - else - :html - end - end - # Receives an array of mimes and return the first user sent mime that # matches the order array. # diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7a17023ed2..ea9f0f99c2 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -30,6 +30,14 @@ module ActionDispatch METHOD end + def self.new(env) + if request = env["action_dispatch.request"] && request.instance_of?(self) + return request + end + + super + end + def key?(key) @env.key?(key) end @@ -119,36 +127,7 @@ module ActionDispatch # delimited list in the case of multiple chained proxies; the last # address which is not trusted is the originating IP. def remote_ip - remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/) - - unless remote_addr_list.blank? - not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES || addr =~ ActionController::Base.trusted_proxies} - return not_trusted_addrs.first unless not_trusted_addrs.empty? - end - remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',') - - if @env.include? 'HTTP_CLIENT_IP' - if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP']) - # We don't know which came from the proxy, and which from the user - raise ActionController::ActionControllerError.new <<EOM -IP spoofing attack?! -HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} -HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect} -EOM - end - - return @env['HTTP_CLIENT_IP'] - end - - if remote_ips - while remote_ips.size > 1 && (TRUSTED_PROXIES =~ remote_ips.last.strip || ActionController::Base.trusted_proxies =~ remote_ips.last.strip) - remote_ips.pop - end - - return remote_ips.last.strip - end - - @env['REMOTE_ADDR'] + (@env["action_dispatch.remote_ip"] || ip).to_s end # Returns the lowercase name of the HTTP server software. diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index f299306ff4..9cfe5a5ea9 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,31 +32,38 @@ module ActionDispatch # :nodoc: # end # end class Response < Rack::Response - include ActionDispatch::Http::Cache::Response - attr_accessor :request, :blank attr_writer :header, :sending_file alias_method :headers=, :header= - def initialize - @status = 200 - @header = {} - @cache_control = {} + module Setup + def initialize(status = 200, header = {}, body = []) + @writer = lambda { |x| @body << x } + @block = nil + @length = 0 - @writer = lambda { |x| @body << x } - @block = nil - @length = 0 + @status, @header = status, header + self.body = body - @body, @cookie = [], [] - @sending_file = false + @cookie = [] + @sending_file = false - @blank = false - @etag = nil + @blank = false + + if content_type = self["Content-Type"] + type, charset = content_type.split(/;\s*charset=/) + @content_type = Mime::Type.lookup(type) + @charset = charset || "UTF-8" + end - yield self if block_given? + yield self if block_given? + end end + include Setup + include ActionDispatch::Http::Cache::Response + def status=(status) @status = Rack::Utils.status_code(status) end @@ -76,6 +83,18 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message + def respond_to?(method) + if method.to_sym == :to_path + @body.respond_to?(:to_path) + else + super + end + end + + def to_path + @body.to_path + end + def body str = '' each { |part| str << part.to_s } @@ -120,7 +139,7 @@ module ActionDispatch # :nodoc: assign_default_content_type_and_charset! handle_conditional_get! self["Set-Cookie"] = @cookie.join("\n") unless @cookie.blank? - self["ETag"] = @etag if @etag + self["ETag"] = @_etag if @_etag super end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 40ceb5a9b6..b64a83c62e 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -3,7 +3,7 @@ module ActionDispatch module URL # Returns the complete URL used for this request. def url - protocol + host_with_port + request_uri + protocol + host_with_port + fullpath end # Returns 'https://' if this is an SSL request and 'http://' otherwise. @@ -81,42 +81,15 @@ module ActionDispatch parts[0..-(tld_length+2)] end - # Returns the query string, accounting for server idiosyncrasies. - def query_string - @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '') + def subdomain(tld_length = 1) + subdomains(tld_length).join('.') end # Returns the request URI, accounting for server idiosyncrasies. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. def request_uri - if uri = @env['REQUEST_URI'] - # Remove domain, which webrick puts into the request_uri. - (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri - else - # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. - uri = @env['PATH_INFO'].to_s - - if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) - uri = uri.sub(/#{script_filename}\//, '') - end - - env_qs = @env['QUERY_STRING'].to_s - uri += "?#{env_qs}" unless env_qs.empty? - - if uri.blank? - @env.delete('REQUEST_URI') - else - @env['REQUEST_URI'] = uri - end - end - end - - # Returns the interpreted \path to requested resource after all the installation - # directory of this application was taken into account. - def path - path = request_uri.to_s[/\A[^\?]*/] - path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') - path + ActiveSupport::Deprecation.warn "Using #request_uri is deprecated. Use fullpath instead.", caller + fullpath end private diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 7cf75ffe63..d07841218a 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -37,7 +37,7 @@ module ActionDispatch def initialize(app, prepare_each_request = false) @app, @prepare_each_request = app, prepare_each_request - run_callbacks(:prepare) unless @prepare_each_request + run_callbacks(:prepare) end def call(env) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 522982e202..f4c4324fb0 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,4 +1,3 @@ -require 'active_support/json' require 'action_dispatch/http/request' module ActionDispatch diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb new file mode 100644 index 0000000000..c7d710b98e --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -0,0 +1,51 @@ +module ActionDispatch + class RemoteIp + class IpSpoofAttackError < StandardError ; end + + class RemoteIpGetter + def initialize(env, check_ip_spoofing, trusted_proxies) + @env = env + @check_ip_spoofing = check_ip_spoofing + @trusted_proxies = trusted_proxies + end + + def remote_addrs + @remote_addrs ||= begin + list = @env['REMOTE_ADDR'] ? @env['REMOTE_ADDR'].split(/[,\s]+/) : [] + list.reject { |addr| addr =~ @trusted_proxies } + end + end + + def to_s + return remote_addrs.first if remote_addrs.any? + + forwarded_ips = @env['HTTP_X_FORWARDED_FOR'] ? @env['HTTP_X_FORWARDED_FOR'].strip.split(/[,\s]+/) : [] + + if client_ip = @env['HTTP_CLIENT_IP'] + if @check_ip_spoofing && !forwarded_ips.include?(client_ip) + # We don't know which came from the proxy, and which from the user + raise IpSpoofAttackError, "IP spoofing attack?!" \ + "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \ + "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" + end + return client_ip + end + + return forwarded_ips.reject { |ip| ip =~ @trusted_proxies }.last || @env["REMOTE_ADDR"] + end + end + + def initialize(app, check_ip_spoofing = true, trusted_proxies = nil) + @app = app + @check_ip_spoofing = check_ip_spoofing + regex = '(^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.)' + regex << "|(#{trusted_proxies})" if trusted_proxies + @trusted_proxies = Regexp.new(regex, "i") + end + + def call(env) + env["action_dispatch.remote_ip"] = RemoteIpGetter.new(env, @check_ip_spoofing, @trusted_proxies) + @app.call(env) + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 04a101dbb2..22da82479e 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,5 +1,4 @@ require 'active_support/core_ext/hash/keys' -require 'rack/request' module ActionDispatch module Session @@ -177,9 +176,8 @@ module ActionDispatch if key.blank? raise ArgumentError, 'A key is required to write a ' + 'cookie containing the session data. Use ' + - 'config.action_controller.session = { :key => ' + - '"_myapp_session", :secret => "some secret phrase" } in ' + - 'config/application.rb' + 'config.action_controller.session_store :cookie_store, { :key => ' + + '"_myapp_session" } in config/application.rb' end end @@ -193,10 +191,9 @@ module ActionDispatch if secret.blank? raise ArgumentError, "A secret is required to generate an " + "integrity hash for cookie session data. Use " + - "config.action_controller.session = { :key => " + - "\"_myapp_session\", :secret => \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\" } " + - "in config/environment.rb" + "config.cookie_secret = \"some secret phrase of at " + + "least #{SECRET_MIN_LENGTH} characters\"" + + "in config/application.rb" end if secret.length < SECRET_MIN_LENGTH diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 18a2922fa7..5c5362ce4a 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -58,7 +58,7 @@ module ActionDispatch if lazy_compare?(@klass) && lazy_compare?(middleware) normalize(@klass) == normalize(middleware) else - klass == ActiveSupport::Inflector.constantize(middleware.to_s) + klass.name == middleware.to_s end end end @@ -122,7 +122,11 @@ module ActionDispatch find_all { |middleware| middleware.active? } end - def build(app) + def build(app = nil, &blk) + app ||= blk + + raise "MiddlewareStack#build requires an app" unless app + active.reverse.inject(app) { |a, e| e.build(a) } end end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 335daafc01..e486bd4079 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -5,6 +5,9 @@ module ActionDispatch class Railtie < Rails::Railtie railtie_name :action_dispatch + config.action_dispatch.x_sendfile_header = "X-Sendfile" + config.action_dispatch.ip_spoofing_check = true + # Prepare dispatcher callbacks and run 'prepare' callbacks initializer "action_dispatch.prepare_dispatcher" do |app| # TODO: This used to say unless defined?(Dispatcher). Find out why and fix. diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 335c9edb98..5bc3205c51 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -205,6 +205,7 @@ module ActionDispatch autoload :Mapper, 'action_dispatch/routing/mapper' autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' + autoload :UrlFor, 'action_dispatch/routing/url_for' SEPARATORS = %w( / . ? ) HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index 8ce6b2f6d5..dd650e83d9 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -1,5 +1,30 @@ module ActionDispatch module Routing + class RouteSet + attr_accessor :controller_namespaces + + CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/ + + def controller_constraints + @controller_constraints ||= begin + namespaces = controller_namespaces + in_memory_controller_namespaces + source = namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" } + source << CONTROLLER_REGEXP.source + Regexp.compile(source.sort.reverse.join('|')) + end + end + + def in_memory_controller_namespaces + namespaces = Set.new + ActionController::Base.subclasses.each do |klass| + controller_name = klass.underscore + namespaces << controller_name.split('/')[0...-1].join('/') + end + namespaces.delete('') + namespaces + end + end + # Mapper instances are used to build routes. The object passed to the draw # block in config/routes.rb is a Mapper instance. # @@ -244,14 +269,15 @@ module ActionDispatch attr_reader :collection_methods, :member_methods, :new_methods attr_reader :path_prefix, :name_prefix, :path_segment attr_reader :plural, :singular - attr_reader :options + attr_reader :options, :defaults - def initialize(entities, options) + def initialize(entities, options, defaults) @plural ||= entities @singular ||= options[:singular] || plural.to_s.singularize @path_segment = options.delete(:as) || @plural @options = options + @defaults = defaults arrange_actions add_default_actions @@ -280,7 +306,7 @@ module ActionDispatch def new_path new_action = self.options[:path_names][:new] if self.options[:path_names] - new_action ||= ActionController::Base.resources_path_names[:new] + new_action ||= self.defaults[:path_names][:new] @new_path ||= "#{path}/#{new_action}" end @@ -370,7 +396,7 @@ module ActionDispatch end class SingletonResource < Resource #:nodoc: - def initialize(entity, options) + def initialize(entity, options, defaults) @singular = @plural = entity options[:controller] ||= @singular.to_s.pluralize super @@ -717,7 +743,7 @@ module ActionDispatch private def map_resource(entities, options = {}, &block) - resource = Resource.new(entities, options) + resource = Resource.new(entities, options, :path_names => @set.resources_path_names) with_options :controller => resource.controller do |map| map_associations(resource, options) @@ -734,7 +760,7 @@ module ActionDispatch end def map_singleton_resource(entities, options = {}, &block) - resource = SingletonResource.new(entities, options) + resource = SingletonResource.new(entities, options, :path_names => @set.resources_path_names) with_options :controller => resource.controller do |map| map_associations(resource, options) @@ -826,7 +852,7 @@ module ActionDispatch actions.each do |action| [method].flatten.each do |m| action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= ActionController::Base.resources_path_names[action] || action + action_path ||= @set.resources_path_names[action] || action map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8de68b3174..0b7b09ee7a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/except' + module ActionDispatch module Routing class Mapper @@ -36,7 +38,7 @@ module ActionDispatch end def to_route - [ app, conditions, requirements, defaults, @options[:as] ] + [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ] end private @@ -64,7 +66,7 @@ module ActionDispatch # match "account/overview" def using_match_shorthand?(args, options) - args.present? && options.except(:via).empty? && !args.first.include?(':') + args.present? && options.except(:via, :anchor).empty? && !args.first.include?(':') end def normalize_path(path) @@ -85,10 +87,9 @@ module ActionDispatch end def requirements - @requirements ||= returning(@options[:constraints] || {}) do |requirements| + @requirements ||= (@options[:constraints] || {}).tap do |requirements| requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints] @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) } - requirements[:controller] ||= @set.controller_constraints end end @@ -175,9 +176,15 @@ module ActionDispatch end def match(*args) - @set.add_route(*Mapping.new(@set, @scope, args).to_route) + mapping = Mapping.new(@set, @scope, args).to_route + @set.add_route(*mapping) self end + + def default_url_options=(options) + @set.default_url_options = options + end + alias_method :default_url_options, :default_url_options= end module HttpHelpers @@ -283,7 +290,7 @@ module ActionDispatch end def namespace(path) - scope(path.to_s, :name_prefix => path.to_s, :namespace => path.to_s) { yield } + scope(path.to_s, :name_prefix => path.to_s, :controller_namespace => path.to_s) { yield } end def constraints(constraints = {}) @@ -294,6 +301,7 @@ module ActionDispatch options = args.extract_options! options = (@scope[:options] || {}).merge(options) + options[:anchor] = true unless options.key?(:anchor) if @scope[:name_prefix] && !options[:as].blank? options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}" @@ -318,12 +326,12 @@ module ActionDispatch parent ? "#{parent}_#{child}" : child end - def merge_namespace_scope(parent, child) + def merge_controller_namespace_scope(parent, child) parent ? "#{parent}/#{child}" : child end def merge_controller_scope(parent, child) - @scope[:namespace] ? "#{@scope[:namespace]}/#{child}" : child + @scope[:controller_namespace] ? "#{@scope[:controller_namespace]}/#{child}" : child end def merge_resources_path_names_scope(parent, child) @@ -367,9 +375,9 @@ module ActionDispatch def actions if only = options[:only] - only.map(&:to_sym) + Array(only).map(&:to_sym) elsif except = options[:except] - default_actions - except.map(&:to_sym) + default_actions - Array(except).map(&:to_sym) else default_actions end @@ -443,7 +451,7 @@ module ActionDispatch def resource(*resources, &block) options = resources.extract_options! - if verify_common_behavior_for(:resource, resources, options, &block) + if apply_common_behavior_for(:resource, resources, options, &block) return self end @@ -451,7 +459,10 @@ module ActionDispatch scope(:path => resource.name.to_s, :controller => resource.controller) do with_scope_level(:resource, resource) do - yield if block_given? + + scope(:name_prefix => resource.name.to_s) do + yield if block_given? + end get :show if resource.actions.include?(:show) post :create if resource.actions.include?(:create) @@ -468,7 +479,7 @@ module ActionDispatch def resources(*resources, &block) options = resources.extract_options! - if verify_common_behavior_for(:resources, resources, options, &block) + if apply_common_behavior_for(:resources, resources, options, &block) return self end @@ -534,6 +545,21 @@ module ActionDispatch end end + def mount(app, options = nil) + if options + path = options.delete(:at) + else + options = app + app, path = options.find { |k, v| k.respond_to?(:call) } + options.delete(app) if app + end + + raise "A rack application must be specified" unless path + + match(path, options.merge(:to => app, :anchor => false)) + self + end + def match(*args) options = args.extract_options! @@ -591,7 +617,7 @@ module ActionDispatch path_names[name.to_sym] || name.to_s end - def verify_common_behavior_for(method, resources, options, &block) + def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index e6e44d3546..6f37eadd6b 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -4,7 +4,7 @@ module ActionDispatch attr_reader :app, :conditions, :defaults, :name attr_reader :path, :requirements - def initialize(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + def initialize(app, conditions, requirements, defaults, name, anchor) @app = app @defaults = defaults @name = name @@ -17,7 +17,7 @@ module ActionDispatch if path = conditions[:path_info] @path = path - conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS) + conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor) end @conditions = conditions.inject({}) { |h, (k, v)| diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index dcf98b729b..722be432c7 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,5 +1,6 @@ require 'rack/mount' require 'forwardable' +require 'action_dispatch/routing/deprecated_mapper' module ActionDispatch module Routing @@ -11,8 +12,8 @@ module ActionDispatch PARAMETERS_KEY = 'action_dispatch.request.path_parameters' class Dispatcher - def initialize(options = {}) - defaults = options[:defaults] + def initialize(options={}) + @defaults = options[:defaults] @glob_param = options.delete(:glob) end @@ -20,7 +21,8 @@ module ActionDispatch params = env[PARAMETERS_KEY] prepare_params!(params) - unless controller = controller(params) + # Just raise undefined constant errors if a controller was specified as default. + unless controller = controller(params, @defaults.key?(:controller)) return [404, {'X-Cascade' => 'pass'}, []] end @@ -39,14 +41,13 @@ module ActionDispatch end end - def controller(params) + def controller(params, raise_error=true) if params && params.has_key?(:controller) controller = "#{params[:controller].camelize}Controller" ActiveSupport::Inflector.constantize(controller) end rescue NameError => e - raise unless e.message.include?(controller) - nil + raise ActionController::RoutingError, e.message, e.backtrace if raise_error end private @@ -59,7 +60,6 @@ module ActionDispatch end end - # A NamedRouteCollection instance is a collection of named routes, and also # maintains an anonymous module that can be used to install helpers for the # named routes. @@ -168,63 +168,25 @@ module ActionDispatch selector = url_helper_name(name, kind) hash_access_method = hash_access_name(name, kind) - # We use module_eval to avoid leaks. - # - # def users_url(*args) - # if args.empty? || Hash === args.first - # options = hash_for_users_url(args.first || {}) - # else - # options = hash_for_users_url(args.extract_options!) - # default = default_url_options(options) if self.respond_to?(:default_url_options, true) - # options = (default ||= {}).merge(options) - # - # keys = [] - # keys -= options.keys if args.size < keys.size - 1 - # - # args = args.zip(keys).inject({}) do |h, (v, k)| - # h[k] = v - # h - # end - # - # # Tell url_for to skip default_url_options - # options[:use_defaults] = false - # options.merge!(args) - # end - # - # url_for(options) - # end @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 def #{selector}(*args) - if args.empty? || Hash === args.first - options = #{hash_access_method}(args.first || {}) - else - options = #{hash_access_method}(args.extract_options!) - default = default_url_options(options) if self.respond_to?(:default_url_options, true) - options = (default ||= {}).merge(options) - - keys = #{route.segment_keys.inspect} - keys -= options.keys if args.size < keys.size - 1 # take format into account - - args = args.zip(keys).inject({}) do |h, (v, k)| - h[k] = v - h - end - - # Tell url_for to skip default_url_options - options[:use_defaults] = false - options.merge!(args) + options = #{hash_access_method}(args.extract_options!) + + if args.any? + options[:_positional_args] = args + options[:_positional_keys] = #{route.segment_keys.inspect} end url_for(options) end - protected :#{selector} END_EVAL helpers << selector end end - attr_accessor :routes, :named_routes, :controller_namespaces + attr_accessor :routes, :named_routes attr_accessor :disable_clear_and_finalize, :resources_path_names + attr_accessor :default_url_options def self.default_resources_path_names { :new => 'new', :edit => 'edit' } @@ -235,8 +197,10 @@ module ActionDispatch self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names.dup self.controller_namespaces = Set.new + self.default_url_options = {} @disable_clear_and_finalize = false + clear! end def draw(&block) @@ -273,61 +237,155 @@ module ActionDispatch named_routes.install(destinations, regenerate_code) end - def empty? - routes.empty? - end + def url_helpers + @url_helpers ||= begin + router = self - CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/ + Module.new do + extend ActiveSupport::Concern + include UrlFor - def controller_constraints - @controller_constraints ||= begin - namespaces = controller_namespaces + in_memory_controller_namespaces - source = namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" } - source << CONTROLLER_REGEXP.source - Regexp.compile(source.sort.reverse.join('|')) + # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that + # we can include? + # Yes plz - JP + included do + router.install_helpers(self) + end + + define_method(:_router) { router } + end end end - def in_memory_controller_namespaces - namespaces = Set.new - ActionController::Base.subclasses.each do |klass| - controller_name = klass.underscore - namespaces << controller_name.split('/')[0...-1].join('/') - end - namespaces.delete('') - namespaces + def empty? + routes.empty? end - def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil) - route = Route.new(app, conditions, requirements, defaults, name) + def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + route = Route.new(app, conditions, requirements, defaults, name, anchor) @set.add_route(*route) named_routes[name] = route if name routes << route route end - def options_as_params(options) - # If an explicit :controller was given, always make :action explicit - # too, so that action expiry works as expected for things like - # - # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) - # - # (the above is from the unit tests). In the above case, because the - # controller was explicitly given, but no action, the action is implied to - # be "index", not the recalled action of "show". - # - # great fun, eh? - - options_as_params = options.clone - options_as_params[:action] ||= 'index' if options[:controller] - options_as_params[:action] = options_as_params[:action].to_s if options_as_params[:action] - options_as_params - end + class Generator + attr_reader :options, :recall, :set, :script_name, :named_route + + def initialize(options, recall, set, extras = false) + @script_name = options.delete(:script_name) + @named_route = options.delete(:use_route) + @options = options.dup + @recall = recall.dup + @set = set + @extras = extras + + normalize_options! + normalize_controller_action_id! + use_relative_controller! + controller.sub!(%r{^/}, '') if controller + handle_nil_action! + end + + def controller + @controller ||= @options[:controller] + end + + def current_controller + @recall[:controller] + end + + def use_recall_for(key) + if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) + @options[key] = @recall.delete(key) + end + end + + def normalize_options! + # If an explicit :controller was given, always make :action explicit + # too, so that action expiry works as expected for things like + # + # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) + # + # (the above is from the unit tests). In the above case, because the + # controller was explicitly given, but no action, the action is implied to + # be "index", not the recalled action of "show". + + if options[:controller] + options[:action] ||= 'index' + options[:controller] = options[:controller].to_s + end + + if options[:action] + options[:action] = options[:action].to_s + end + end + + # This pulls :controller, :action, and :id out of the recall. + # The recall key is only used if there is no key in the options + # or if the key in the options is identical. If any of + # :controller, :action or :id is not found, don't pull any + # more keys from the recall. + def normalize_controller_action_id! + @recall[:action] ||= 'index' if current_controller + + use_recall_for(:controller) or return + use_recall_for(:action) or return + use_recall_for(:id) + end + + # if the current controller is "foo/bar/baz" and :controller => "baz/bat" + # is specified, the controller becomes "foo/baz/bat" + def use_relative_controller! + if !named_route && different_controller? + old_parts = current_controller.split('/') + size = controller.count("/") + 1 + parts = old_parts[0...-size] << controller + @controller = @options[:controller] = parts.join("/") + end + end + + # This handles the case of :action => nil being explicitly passed. + # It is identical to :action => "index" + def handle_nil_action! + if options.has_key?(:action) && options[:action].nil? + options[:action] = 'index' + end + recall[:action] = options.delete(:action) if options[:action] == 'index' + end - def build_expiry(options, recall) - recall.inject({}) do |expiry, (key, recalled_value)| - expiry[key] = (options.key?(key) && options[key].to_param != recalled_value.to_param) - expiry + def generate + error = ActionController::RoutingError.new("No route matches #{options.inspect}") + path, params = @set.generate(:path_info, named_route, options, recall, opts) + + raise error unless path + + params.reject! {|k,v| !v } + + return [path, params.keys] if @extras + + path << "?#{params.to_query}" if params.any? + "#{script_name}#{path}" + rescue Rack::Mount::RoutingError + raise error + end + + def opts + parameterize = lambda do |name, value| + if name == :controller + value + elsif value.is_a?(Array) + value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') + else + Rack::Mount::Utils.escape_uri(value.to_param) + end + end + {:parameterize => parameterize} + end + + def different_controller? + return false unless current_controller + controller.to_param != current_controller.to_param end end @@ -338,82 +396,44 @@ module ActionDispatch end def generate_extras(options, recall={}) - generate(options, recall, :generate_extras) + generate(options, recall, true) end - def generate(options, recall = {}, method = :generate) - options, recall = options.dup, recall.dup - named_route = options.delete(:use_route) + def generate(options, recall = {}, extras = false) + Generator.new(options, recall, @set, extras).generate + end - options = options_as_params(options) - expire_on = build_expiry(options, recall) + RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash] - recall[:action] ||= 'index' if options[:controller] || recall[:controller] + def url_for(options) + options = default_url_options.merge(options || {}) - if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) - options[:controller] = recall.delete(:controller) + handle_positional_args(options) - if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) - options[:action] = recall.delete(:action) + rewritten_url = "" - if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) - options[:id] = recall.delete(:id) - end - end - end - - options[:controller] = options[:controller].to_s if options[:controller] + path_segments = options.delete(:_path_segments) - if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ - old_parts = recall[:controller].split('/') - new_parts = options[:controller].split('/') - parts = old_parts[0..-(new_parts.length + 1)] + new_parts - options[:controller] = parts.join('/') - end + unless options[:only_path] + rewritten_url << (options[:protocol] || "http") + rewritten_url << "://" unless rewritten_url.match("://") + rewritten_url << rewrite_authentication(options) - options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ + raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] - merged = options.merge(recall) - if options.has_key?(:action) && options[:action].nil? - options.delete(:action) - recall[:action] = 'index' + rewritten_url << options[:host] + rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) end - recall[:action] = options.delete(:action) if options[:action] == 'index' - - opts = {} - opts[:parameterize] = lambda { |name, value| - if name == :controller - value - elsif value.is_a?(Array) - value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') - else - Rack::Mount::Utils.escape_uri(value.to_param) - end - } - unless result = @set.generate(:path_info, named_route, options, recall, opts) - raise ActionController::RoutingError, "No route matches #{options.inspect}" - end + path_options = options.except(*RESERVED_OPTIONS) + path_options = yield(path_options) if block_given? + path = generate(path_options, path_segments || {}) - path, params = result - params.each do |k, v| - if v - params[k] = v - else - params.delete(k) - end - end + # ROUTES TODO: This can be called directly, so script_name should probably be set in the router + rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) + rewritten_url << "##{Rack::Utils.escape(options[:anchor].to_param.to_s)}" if options[:anchor] - if path && method == :generate_extras - [path, params.keys] - elsif path - path << "?#{params.to_query}" if params.any? - path - else - raise ActionController::RoutingError, "No route matches #{options.inspect}" - end - rescue Rack::Mount::RoutingError - raise ActionController::RoutingError, "No route matches #{options.inspect}" + rewritten_url end def call(env) @@ -431,9 +451,9 @@ module ActionDispatch end req = Rack::Request.new(env) - @set.recognize(req) do |route, params| + @set.recognize(req) do |route, matches, params| dispatcher = route.app - if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params) + if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false) dispatcher.prepare_params!(params) return params end @@ -441,6 +461,30 @@ module ActionDispatch raise ActionController::RoutingError, "No route matches #{path.inspect}" end + + private + def handle_positional_args(options) + return unless args = options.delete(:_positional_args) + + keys = options.delete(:_positional_keys) + keys -= options.keys if args.size < keys.size - 1 # take format into account + + args = args.zip(keys).inject({}) do |h, (v, k)| + h[k] = v + h + end + + # Tell url_for to skip default_url_options + options.merge!(args) + end + + def rewrite_authentication(options) + if options[:user] && options[:password] + "#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@" + else + "" + 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 new file mode 100644 index 0000000000..ec78f53fa6 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -0,0 +1,139 @@ +module ActionDispatch + module Routing + # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse + # is also possible: an URL can be generated from one of your routing definitions. + # URL generation functionality is centralized in this module. + # + # See ActionDispatch::Routing and ActionController::Resources for general + # information about routing and routes.rb. + # + # <b>Tip:</b> If you need to generate URLs from your models or some other place, + # then ActionController::UrlFor is what you're looking for. Read on for + # an introduction. + # + # == URL generation from parameters + # + # As you may know, some functions - such as ActionController::Base#url_for + # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set + # of parameters. For example, you've probably had the chance to write code + # like this in one of your views: + # + # <%= link_to('Click here', :controller => 'users', + # :action => 'new', :message => 'Welcome!') %> + # + # #=> Generates a link to: /users/new?message=Welcome%21 + # + # link_to, and all other functions that require URL generation functionality, + # actually use ActionController::UrlFor under the hood. And in particular, + # they use the ActionController::UrlFor#url_for method. One can generate + # the same path as the above example by using the following code: + # + # include UrlFor + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :only_path => true) + # # => "/users/new?message=Welcome%21" + # + # Notice the <tt>:only_path => true</tt> part. This is because UrlFor has no + # information about the website hostname that your Rails app is serving. So if you + # want to include the hostname as well, then you must also pass the <tt>:host</tt> + # argument: + # + # include UrlFor + # url_for(:controller => 'users', + # :action => 'new', + # :message => 'Welcome!', + # :host => 'www.example.com') # Changed this. + # # => "http://www.example.com/users/new?message=Welcome%21" + # + # By default, all controllers and views have access to a special version of url_for, + # that already knows what the current hostname is. So if you use url_for in your + # controllers or your views, then you don't need to explicitly pass the <tt>:host</tt> + # argument. + # + # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. + # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' + # in full. However, mailers don't have hostname information, and what's why you'll still + # have to specify the <tt>:host</tt> argument when generating URLs in mailers. + # + # + # == URL generation for named routes + # + # UrlFor also allows one to access methods that have been auto-generated from + # named routes. For example, suppose that you have a 'users' resource in your + # <b>routes.rb</b>: + # + # map.resources :users + # + # This generates, among other things, the method <tt>users_path</tt>. By default, + # this method is accessible from your controllers, views and mailers. If you need + # to access this auto-generated method from other places (such as a model), then + # you can do that by including ActionController::UrlFor in your class: + # + # class User < ActiveRecord::Base + # include ActionController::UrlFor + # + # def base_uri + # user_path(self) + # end + # end + # + # User.find(1).base_uri # => "/users/1" + # + module UrlFor + extend ActiveSupport::Concern + + included do + # TODO: with_routing extends @controller with url_helpers, trickling down to including this module which overrides its default_url_options + unless method_defined?(:default_url_options) + # Including in a class uses an inheritable hash. Modules get a plain hash. + if respond_to?(:class_attribute) + class_attribute :default_url_options + else + mattr_accessor :default_url_options + remove_method :default_url_options + end + + self.default_url_options = {} + end + end + + def url_options + default_url_options + end + + # Generate a url based on the options provided, default_url_options and the + # routes defined in routes.rb. The following options are supported: + # + # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. + # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. + # * <tt>:host</tt> - Specifies the host the link should be targeted at. + # If <tt>:only_path</tt> is false, this option must be + # provided either explicitly, or via +default_url_options+. + # * <tt>:port</tt> - Optionally specify the port to connect to. + # * <tt>:anchor</tt> - An anchor name to be appended to the path. + # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" + # + # Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to + # +url_for+ is forwarded to the Routes module. + # + # Examples: + # + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' + # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + def url_for(options = nil) + case options + when String + options + when nil, Hash + _router.url_for(url_options.merge(options || {})) + else + polymorphic_url(options) + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index c2486d3730..937c9f48d2 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -132,16 +132,21 @@ module ActionDispatch end def normalize_argument_to_redirection(fragment) - after_routing = @controller.url_for(fragment) - if after_routing =~ %r{^\w+://.*} - after_routing - else - # FIXME - this should probably get removed. - if after_routing.first != '/' - after_routing = '/' + after_routing + case fragment + when %r{^\w[\w\d+.-]*:.*} + fragment + when String + if fragment =~ %r{^\w[\w\d+.-]*:.*} + fragment + else + @request.protocol + @request.host_with_port + fragment end - @request.protocol + @request.host_with_port + after_routing - end + when :back + raise RedirectBackError unless refer = @request.headers["Referer"] + refer + else + @controller.url_for(fragment) + end.gsub(/[\r\n]/, '') end def validate_request! diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 0c33539b4a..1d7e8090e4 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -80,7 +80,7 @@ module ActionDispatch expected_path = "/#{expected_path}" unless expected_path[0] == ?/ # Load routes.rb if it hasn't been loaded. - generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults) + generated_path, extra_keys = @router.generate_extras(options, defaults) found_extras = options.reject {|k, v| ! extra_keys.include? k} msg = build_message(message, "found extras <?>, not <?>", found_extras, extras) @@ -125,7 +125,7 @@ module ActionDispatch end # A helper to make it easier to test different route configurations. - # This method temporarily replaces ActionController::Routing::Routes + # This method temporarily replaces @router # with a new RouteSet instance. # # The new instance is yielded to the passed block. Typically the block @@ -142,22 +142,19 @@ module ActionDispatch # end # def with_routing - real_routes = ActionController::Routing::Routes - ActionController::Routing.module_eval { remove_const :Routes } - - temporary_routes = ActionController::Routing::RouteSet.new - ActionController::Routing.module_eval { const_set :Routes, temporary_routes } - - yield temporary_routes + old_routes, @router = @router, ActionDispatch::Routing::RouteSet.new + old_controller, @controller = @controller, @controller.clone if @controller + _router = @router + @controller.singleton_class.send(:send, :include, @router.url_helpers) if @controller + yield @router ensure - if ActionController::Routing.const_defined? :Routes - ActionController::Routing.module_eval { remove_const :Routes } - end - ActionController::Routing.const_set(:Routes, real_routes) if real_routes + @router = old_routes + @controller = old_controller if @controller end + # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if @controller && ActionController::Routing::Routes.named_routes.helpers.include?(selector) + if @controller && @router.named_routes.helpers.include?(selector) @controller.send(selector, *args, &block) else super @@ -174,7 +171,7 @@ module ActionDispatch request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method request.path = path - params = ActionController::Routing::Routes.recognize_path(path, { :method => request.method }) + params = @router.recognize_path(path, { :method => request.method }) request.path_parameters = params.with_indifferent_access request diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2093bb3a0e..0aff4250c1 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -1,6 +1,6 @@ require 'stringio' require 'uri' -require 'active_support/core_ext/object/metaclass' +require 'active_support/core_ext/object/singleton_class' require 'rack/test' module ActionDispatch @@ -162,12 +162,31 @@ module ActionDispatch # A running counter of the number of requests processed. attr_accessor :request_count + include ActionDispatch::Routing::UrlFor + # Create and initialize a new Session instance. def initialize(app) @app = app + + # If the app is a Rails app, make url_helpers available on the session + # This makes app.url_for and app.foo_path available in the console + if app.respond_to?(:routes) && app.routes.respond_to?(:url_helpers) + singleton_class.class_eval { include app.routes.url_helpers } + end + reset! end + def url_options + opts = super.reverse_merge( + :host => host, + :protocol => https? ? "https" : "http" + ) + + opts.merge!(:port => 443) if !opts.key?(:port) && https? + opts + end + # Resets the instance. This can be used to reset the state information # in an existing session instance, so it can be used from a clean-slate # condition. @@ -187,12 +206,10 @@ module ActionDispatch unless defined? @named_routes_configured # install the named routes in this session instance. - klass = metaclass - ActionController::Routing::Routes.install_helpers(klass) + klass = singleton_class # the helpers are made protected by default--we make them public for # easier access during testing and troubleshooting. - klass.module_eval { public *ActionController::Routing::Routes.named_routes.helpers } @named_routes_configured = true end end @@ -221,14 +238,6 @@ module ActionDispatch @host = name end - # Returns the URL for the given options, according to the rules specified - # in the application's routes. - def url_for(options) - controller ? - controller.url_for(options) : - generic_url_rewriter.rewrite(options) - end - private # Performs the actual request. @@ -273,26 +282,14 @@ module ActionDispatch @request_count += 1 @request = ActionDispatch::Request.new(session.last_request.env) - @response = ActionDispatch::TestResponse.from_response(@mock_session.last_response) + response = @mock_session.last_response + @response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body) @html_document = nil @controller = session.last_request.env['action_controller.instance'] return response.status end - - # Get a temporary URL writer object - def generic_url_rewriter - env = { - 'REQUEST_METHOD' => "GET", - 'QUERY_STRING' => "", - "REQUEST_URI" => "/", - "HTTP_HOST" => host, - "SERVER_PORT" => https? ? "443" : "80", - "HTTPS" => https? ? "on" : "off" - } - ActionController::UrlRewriter.new(ActionDispatch::Request.new(env), {}) - end end module Runner @@ -364,6 +361,14 @@ module ActionDispatch end end + extend ActiveSupport::Concern + include ActionDispatch::Routing::UrlFor + + def url_options + reset! unless @integration_session + @integration_session.url_options + end + # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) reset! unless @integration_session diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index eae703e1b6..d4eecac2de 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,3 +1,5 @@ +require 'action_dispatch/middleware/flash' + module ActionDispatch module TestProcess def assigns(key = nil) |