aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch')
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb60
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb15
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb23
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb211
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb9
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb67
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb133
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb16
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb51
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb40
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/head.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb45
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb90
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb45
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb70
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb12
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb12
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb177
-rw-r--r--actionpack/lib/action_dispatch/routing.rb7
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb121
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb283
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb28
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb63
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb248
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb20
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb44
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb39
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb9
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/tag.rb5
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb52
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb12
46 files changed, 1555 insertions, 585 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 5ee4c044ea..0d6015d993 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
module ActionDispatch
module Http
@@ -18,12 +17,21 @@ module ActionDispatch
env[HTTP_IF_NONE_MATCH]
end
+ def if_none_match_etags
+ (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
+ etag.gsub(/^\"|\"$/, "")
+ end
+ end
+
def not_modified?(modified_at)
if_modified_since && modified_at && if_modified_since >= modified_at
end
def etag_matches?(etag)
- if_none_match && if_none_match == etag
+ if etag
+ etag = etag.gsub(/^\"|\"$/, "")
+ if_none_match_etags.include?(etag)
+ end
end
# Check response freshness (Last-Modified and ETag) against request
@@ -84,17 +92,37 @@ module ActionDispatch
LAST_MODIFIED = "Last-Modified".freeze
ETAG = "ETag".freeze
CACHE_CONTROL = "Cache-Control".freeze
+ SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate]
- def prepare_cache_control!
- @cache_control = {}
- @etag = self[ETAG]
-
+ def cache_control_segments
if cache_control = self[CACHE_CONTROL]
- cache_control.split(/,\s*/).each do |segment|
- first, last = segment.split("=")
- @cache_control[first.to_sym] = last || true
+ cache_control.delete(' ').split(',')
+ else
+ []
+ end
+ end
+
+ def cache_control_headers
+ cache_control = {}
+
+ cache_control_segments.each do |segment|
+ directive, argument = segment.split('=', 2)
+
+ if SPESHUL_KEYS.include? directive
+ key = directive.tr('-', '_')
+ cache_control[key.to_sym] = argument || true
+ else
+ cache_control[:extras] ||= []
+ cache_control[:extras] << segment
end
end
+
+ cache_control
+ end
+
+ def prepare_cache_control!
+ @cache_control = cache_control_headers
+ @etag = self[ETAG]
end
def handle_conditional_get!
@@ -110,14 +138,24 @@ module ActionDispatch
MUST_REVALIDATE = "must-revalidate".freeze
def set_conditional_cache_control!
- return if self[CACHE_CONTROL].present?
+ control = {}
+ cc_headers = cache_control_headers
+ if extras = cc_headers.delete(:extras)
+ @cache_control[:extras] ||= []
+ @cache_control[:extras] += extras
+ @cache_control[:extras].uniq!
+ end
- control = @cache_control
+ control.merge! cc_headers
+ control.merge! @cache_control
if control.empty?
headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
elsif control[:no_cache]
headers[CACHE_CONTROL] = NO_CACHE
+ if control[:extras]
+ headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
+ end
else
extras = control[:extras]
max_age = control[:max_age]
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 132b0c82bc..47cf41cfa3 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/duplicable'
@@ -10,8 +9,6 @@ module ActionDispatch
# value of the params hash and all subhashes is passed to it, the value
# or key can be replaced using String#replace or similar method.
#
- # Examples:
- #
# env["action_dispatch.parameter_filter"] = [:password]
# => replaces the value to all keys matching /password/i with "[FILTERED]"
#
@@ -22,7 +19,6 @@ module ActionDispatch
# v.reverse! if k =~ /secret/i
# end
# => reverses the value to all keys matching /secret/i
- #
module FilterParameters
extend ActiveSupport::Concern
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 040b51e040..a3bb25f75a 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -14,17 +14,18 @@ module ActionDispatch
end
def [](header_name)
- if include?(header_name)
- super
- else
- super(env_name(header_name))
- end
+ super env_name(header_name)
+ end
+
+ def fetch(header_name, default=nil, &block)
+ super env_name(header_name), default, &block
end
private
- # Converts a HTTP header name to an environment variable name.
+ # Converts a HTTP header name to an environment variable name if it is
+ # not contained within the headers hash.
def env_name(header_name)
- @@env_cache[header_name]
+ include?(header_name) ? header_name : @@env_cache[header_name]
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 5c48a60469..0f98e84788 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/module/attribute_accessors'
+
module ActionDispatch
module Http
module MimeNegotiation
@@ -78,6 +80,27 @@ module ActionDispatch
@env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
end
+ # Sets the \formats by string extensions. This differs from #format= by allowing you
+ # to set multiple, ordered formats, which is useful when you want to have a fallback.
+ #
+ # In this example, the :iphone format will be used if it's available, otherwise it'll fallback
+ # to the :html format.
+ #
+ # class ApplicationController < ActionController::Base
+ # before_filter :adjust_format_for_iphone_with_html_fallback
+ #
+ # private
+ # def adjust_format_for_iphone_with_html_fallback
+ # request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
+ # end
+ # end
+ def formats=(extensions)
+ parameters[:format] = extensions.first.to_s
+ @env["action_dispatch.request.formats"] = extensions.collect do |extension|
+ Mime::Type.lookup_by_extension(extension)
+ 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/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 2152351703..f86ae26b8a 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,11 +1,11 @@
require 'set'
require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/starts_ends_with'
module Mime
class Mimes < Array
def symbols
- @symbols ||= map {|m| m.to_sym }
+ @symbols ||= map { |m| m.to_sym }
end
%w(<< concat shift unshift push pop []= clear compact! collect!
@@ -24,9 +24,16 @@ module Mime
EXTENSION_LOOKUP = {}
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
- def self.[](type)
- return type if type.is_a?(Type)
- Type.lookup_by_extension(type.to_s)
+ class << self
+ def [](type)
+ return type if type.is_a?(Type)
+ Type.lookup_by_extension(type)
+ end
+
+ def fetch(type)
+ return type if type.is_a?(Type)
+ EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
+ end
end
# Encapsulates the notion of a mime type. Can be used at render time, for example, with:
@@ -38,7 +45,7 @@ module Mime
# respond_to do |format|
# format.html
# format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
- # format.xml { render :xml => @people.to_xml }
+ # format.xml { render :xml => @people }
# end
# end
# end
@@ -53,36 +60,94 @@ module Mime
cattr_reader :browser_generated_types
attr_reader :symbol
+ @register_callbacks = []
+
# A simple helper class used in parsing the accept header
class AcceptItem #:nodoc:
- attr_accessor :order, :name, :q
+ attr_accessor :index, :name, :q
+ alias :to_s :name
- def initialize(order, name, q=nil)
- @order = order
- @name = name.strip
- q ||= 0.0 if @name == Mime::ALL # default wildcard match to end of list
+ def initialize(index, name, q = nil)
+ @index = index
+ @name = name
+ q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list
@q = ((q || 1.0).to_f * 100).to_i
end
- def to_s
- @name
- end
-
def <=>(item)
- result = item.q <=> q
- result = order <=> item.order if result == 0
+ result = item.q <=> @q
+ result = @index <=> item.index if result == 0
result
end
def ==(item)
- name == (item.respond_to?(:name) ? item.name : item)
+ @name == item.to_s
end
end
- class << self
+ class AcceptList < Array #:nodoc:
+ def assort!
+ sort!
+
+ # Take care of the broken text/xml entry by renaming or deleting it
+ if text_xml_idx && app_xml_idx
+ app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two
+ exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
+ delete_at(text_xml_idx) # delete text_xml from the list
+ elsif text_xml_idx
+ text_xml.name = Mime::XML.to_s
+ end
+
+ # Look for more specific XML-based types and sort them ahead of app/xml
+ if app_xml_idx
+ idx = app_xml_idx
+
+ while idx < length
+ type = self[idx]
+ break if type.q < app_xml.q
+
+ if type.name.ends_with? '+xml'
+ self[app_xml_idx], self[idx] = self[idx], app_xml
+ @app_xml_idx = idx
+ end
+ idx += 1
+ end
+ end
+
+ map! { |i| Mime::Type.lookup(i.name) }.uniq!
+ to_a
+ end
+
+ private
+ def text_xml_idx
+ @text_xml_idx ||= index('text/xml')
+ end
+
+ def app_xml_idx
+ @app_xml_idx ||= index(Mime::XML.to_s)
+ end
+
+ def text_xml
+ self[text_xml_idx]
+ end
+
+ def app_xml
+ self[app_xml_idx]
+ end
+
+ def exchange_xml_items
+ self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml
+ @app_xml_idx, @text_xml_idx = text_xml_idx, app_xml_idx
+ end
+ end
+ class << self
TRAILING_STAR_REGEXP = /(text|application)\/\*/
- Q_SEPARATOR_REGEXP = /;\s*q=/
+ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
+
+ def register_callback(&block)
+ @register_callbacks << block
+ end
def lookup(string)
LOOKUP[string]
@@ -99,91 +164,51 @@ module Mime
end
def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
- Mime.const_set(symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms))
+ Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms))
- SET << Mime.const_get(symbol.to_s.upcase)
+ new_mime = Mime.const_get(symbol.upcase)
+ SET << new_mime
([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup
([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last }
+
+ @register_callbacks.each do |callback|
+ callback.call(new_mime)
+ end
end
def parse(accept_header)
if accept_header !~ /,/
- accept_header = accept_header.split(Q_SEPARATOR_REGEXP).first
- if accept_header =~ TRAILING_STAR_REGEXP
- parse_data_with_trailing_star($1)
- else
- [Mime::Type.lookup(accept_header)]
- end
+ accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
+ parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)]
else
- # keep track of creation order to keep the subsequent sort stable
- list, index = [], 0
- accept_header.split(/,/).each do |header|
- params, q = header.split(Q_SEPARATOR_REGEXP)
+ list, index = AcceptList.new, 0
+ accept_header.split(',').each do |header|
+ params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
if params.present?
params.strip!
- if params =~ TRAILING_STAR_REGEXP
- parse_data_with_trailing_star($1).each do |m|
- list << AcceptItem.new(index, m.to_s, q)
- index += 1
- end
- else
- list << AcceptItem.new(index, params, q)
- index += 1
- end
- end
- end
- list.sort!
-
- # Take care of the broken text/xml entry by renaming or deleting it
- text_xml = list.index("text/xml")
- app_xml = list.index(Mime::XML.to_s)
-
- if text_xml && app_xml
- # set the q value to the max of the two
- list[app_xml].q = [list[text_xml].q, list[app_xml].q].max
-
- # make sure app_xml is ahead of text_xml in the list
- if app_xml > text_xml
- list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
- app_xml, text_xml = text_xml, app_xml
- end
+ params = parse_trailing_star(params) || [params]
- # delete text_xml from the list
- list.delete_at(text_xml)
-
- elsif text_xml
- list[text_xml].name = Mime::XML.to_s
- end
-
- # Look for more specific XML-based types and sort them ahead of app/xml
-
- if app_xml
- idx = app_xml
- app_xml_type = list[app_xml]
-
- while(idx < list.length)
- type = list[idx]
- break if type.q < app_xml_type.q
- if type.name =~ /\+xml$/
- list[app_xml], list[idx] = list[idx], list[app_xml]
- app_xml = idx
+ params.each do |m|
+ list << AcceptItem.new(index, m.to_s, q)
+ index += 1
end
- idx += 1
end
end
-
- list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
- list
+ list.assort!
end
end
- # input: 'text'
- # returned value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]
+ def parse_trailing_star(accept_header)
+ parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
+ end
+
+ # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS,
+ # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>.
#
- # input: 'application'
- # returned value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]
+ # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS,
+ # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>.
def parse_data_with_trailing_star(input)
Mime::SET.select { |m| m =~ input }
end
@@ -192,9 +217,9 @@ module Mime
#
# Usage:
#
- # Mime::Type.unregister(:mobile)
+ # Mime::Type.unregister(:mobile)
def unregister(symbol)
- symbol = symbol.to_s.upcase
+ symbol = symbol.upcase
mime = Mime.const_get(symbol)
Mime.instance_eval { remove_const(symbol) }
@@ -258,18 +283,18 @@ module Mime
@@html_types.include?(to_sym) || @string =~ /html/
end
- def respond_to?(method, include_private = false) #:nodoc:
- super || method.to_s =~ /(\w+)\?$/
- end
-
private
def method_missing(method, *args)
- if method.to_s =~ /(\w+)\?$/
- $1.downcase.to_sym == to_sym
+ if method.to_s.ends_with? '?'
+ method[0..-2].downcase.to_sym == to_sym
else
super
end
end
+
+ def respond_to_missing?(method, include_private = false) #:nodoc:
+ method.to_s.ends_with? '?'
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index d9b63faf5e..9a7b5bc8c7 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -4,6 +4,11 @@ require 'active_support/core_ext/hash/indifferent_access'
module ActionDispatch
module Http
module Parameters
+ def initialize(env)
+ super
+ @symbolized_path_params = nil
+ end
+
# Returns both GET and POST \parameters in a single hash.
def parameters
@env["action_dispatch.request.parameters"] ||= begin
@@ -35,6 +40,10 @@ module ActionDispatch
@env["action_dispatch.request.path_parameters"] ||= {}
end
+ def reset_parameters #:nodoc:
+ @env.delete("action_dispatch.request.parameters")
+ end
+
private
# TODO: Validate that the characters are UTF-8. If they aren't,
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index de014a9c00..b8ebeb408f 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -17,7 +17,10 @@ module ActionDispatch
include ActionDispatch::Http::Upload
include ActionDispatch::Http::URL
- LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze
+ autoload :Session, 'action_dispatch/request/session'
+
+ LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
+
ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
@@ -35,6 +38,17 @@ module ActionDispatch
METHOD
end
+ def initialize(env)
+ super
+ @method = nil
+ @request_method = nil
+ @remote_ip = nil
+ @original_fullpath = nil
+ @fullpath = nil
+ @ip = nil
+ @uuid = nil
+ end
+
def key?(key)
@env.key?(key)
end
@@ -116,9 +130,9 @@ module ActionDispatch
end
# Is this a HEAD request?
- # Equivalent to <tt>request.method_symbol == :head</tt>.
+ # Equivalent to <tt>request.request_method_symbol == :head</tt>.
def head?
- HTTP_METHOD_LOOKUP[method] == :head
+ HTTP_METHOD_LOOKUP[request_method] == :head
end
# Provides access to the request's HTTP headers, for example:
@@ -213,32 +227,42 @@ module ActionDispatch
# TODO This should be broken apart into AD::Request::Session and probably
# be included by the session middleware.
def reset_session
- session.destroy if session && session.respond_to?(:destroy)
- self.session = {}
+ if session && session.respond_to?(:destroy)
+ session.destroy
+ else
+ self.session = {}
+ end
@env['action_dispatch.request.flash_hash'] = nil
end
def session=(session) #:nodoc:
- @env['rack.session'] = session
+ Session.set @env, session
end
def session_options=(options)
- @env['rack.session.options'] = options
+ Session::Options.set @env, options
end
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {})
+ begin
+ @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {})
+ rescue TypeError => e
+ raise ActionController::BadRequest, "Invalid query parameters: #{e.message}"
+ end
end
alias :query_parameters :GET
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {})
+ begin
+ @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {})
+ rescue TypeError => e
+ raise ActionController::BadRequest, "Invalid request parameters: #{e.message}"
+ end
end
alias :request_parameters :POST
-
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
def authorization
@@ -250,7 +274,28 @@ module ActionDispatch
# True if the request came from localhost, 127.0.0.1.
def local?
- LOCALHOST.any? { |local_ip| local_ip === remote_addr && local_ip === remote_ip }
+ LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
+ end
+
+ protected
+
+ # Remove nils from the params hash
+ def deep_munge(hash)
+ hash.each_value do |v|
+ case v
+ when Array
+ v.grep(Hash) { |x| deep_munge(x) }
+ v.compact!
+ when Hash
+ deep_munge(v)
+ end
+ end
+
+ hash
+ end
+
+ def parse_query(qs)
+ deep_munge(super)
end
private
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 84732085f0..11b7534ea4 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,7 +1,6 @@
require 'digest/md5'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/class/attribute_accessors'
+require 'monitor'
module ActionDispatch # :nodoc:
# Represents an HTTP response generated by a controller action. Use it to
@@ -29,7 +28,7 @@ module ActionDispatch # :nodoc:
# class DemoControllerTest < ActionDispatch::IntegrationTest
# def test_print_root_path_to_console
# get('/')
- # puts @response.body
+ # puts response.body
# end
# end
class Response
@@ -41,7 +40,7 @@ module ActionDispatch # :nodoc:
alias_method :headers, :header
delegate :[], :[]=, :to => :@header
- delegate :each, :to => :@body
+ delegate :each, :to => :@stream
# Sets the HTTP response's content MIME type. For example, in the controller
# you could write this:
@@ -51,22 +50,63 @@ module ActionDispatch # :nodoc:
# If a character set has been defined for this response (see charset=) then
# the character set information will also be included in the content type
# information.
- attr_accessor :charset, :content_type
+ attr_accessor :charset
+ attr_reader :content_type
CONTENT_TYPE = "Content-Type".freeze
SET_COOKIE = "Set-Cookie".freeze
LOCATION = "Location".freeze
-
+
cattr_accessor(:default_charset) { "utf-8" }
+ cattr_accessor(:default_headers)
include Rack::Response::Helpers
include ActionDispatch::Http::Cache::Response
+ include MonitorMixin
+
+ class Buffer # :nodoc:
+ def initialize(response, buf)
+ @response = response
+ @buf = buf
+ @closed = false
+ end
+
+ def write(string)
+ raise IOError, "closed stream" if closed?
+
+ @response.commit!
+ @buf.push string
+ end
+
+ def each(&block)
+ @buf.each(&block)
+ end
+
+ def close
+ @response.commit!
+ @closed = true
+ end
+
+ def closed?
+ @closed
+ end
+ end
+
+ attr_reader :stream
def initialize(status = 200, header = {}, body = [])
+ super()
+
+ header = merge_default_headers(header, self.class.default_headers)
+
self.body, self.header, self.status = body, header, status
@sending_file = false
- @blank = false
+ @blank = false
+ @cv = new_cond
+ @committed = false
+ @content_type = nil
+ @charset = nil
if content_type = self[CONTENT_TYPE]
type, charset = content_type.split(/;\s*charset=/)
@@ -79,10 +119,31 @@ module ActionDispatch # :nodoc:
yield self if block_given?
end
+ def await_commit
+ synchronize do
+ @cv.wait_until { @committed }
+ end
+ end
+
+ def commit!
+ synchronize do
+ @committed = true
+ @cv.broadcast
+ end
+ end
+
+ def committed?
+ @committed
+ end
+
def status=(status)
@status = Rack::Utils.status_code(status)
end
+ def content_type=(content_type)
+ @content_type = content_type.to_s
+ end
+
# The response code of the request
def response_code
@status
@@ -100,14 +161,14 @@ module ActionDispatch # :nodoc:
def respond_to?(method)
if method.to_sym == :to_path
- @body.respond_to?(:to_path)
+ stream.respond_to?(:to_path)
else
super
end
end
def to_path
- @body.to_path
+ stream.to_path
end
def body
@@ -121,11 +182,17 @@ module ActionDispatch # :nodoc:
def body=(body)
@blank = true if body == EMPTY
- @body = body.respond_to?(:each) ? body : [body]
+ if body.respond_to?(:to_path)
+ @stream = body
+ else
+ @stream = build_buffer self, munge_body_object(body)
+ end
end
def body_parts
- @body
+ parts = []
+ @stream.each { |x| parts << x }
+ parts
end
def set_cookie(key, value)
@@ -146,21 +213,11 @@ module ActionDispatch # :nodoc:
end
def close
- @body.close if @body.respond_to?(:close)
+ stream.close if stream.respond_to?(:close)
end
def to_a
- assign_default_content_type_and_charset!
- handle_conditional_get!
-
- @header[SET_COOKIE] = @header[SET_COOKIE].join("\n") if @header[SET_COOKIE].respond_to?(:join)
-
- if [204, 304].include?(@status)
- @header.delete CONTENT_TYPE
- [@status, @header, []]
- else
- [@status, @header, self]
- end
+ rack_response @status, @header.to_hash
end
alias prepare! to_a
alias to_ary to_a # For implicit splat on 1.9.2
@@ -184,7 +241,21 @@ module ActionDispatch # :nodoc:
private
- def assign_default_content_type_and_charset!
+ def merge_default_headers(original, default)
+ return original unless default.respond_to?(:merge)
+
+ default.merge(original)
+ end
+
+ def build_buffer(response, body)
+ Buffer.new response, body
+ end
+
+ def munge_body_object(body)
+ body.respond_to?(:each) ? body : [body]
+ end
+
+ def assign_default_content_type_and_charset!(headers)
return if headers[CONTENT_TYPE].present?
@content_type ||= Mime::HTML
@@ -195,5 +266,19 @@ module ActionDispatch # :nodoc:
headers[CONTENT_TYPE] = type
end
+
+ def rack_response(status, header)
+ assign_default_content_type_and_charset!(header)
+ handle_conditional_get!
+
+ header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
+
+ if [204, 304].include?(@status)
+ header.delete CONTENT_TYPE
+ [status, header, []]
+ else
+ [status, header, self]
+ end
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 5ab99d1061..ce8c2729e9 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -4,11 +4,12 @@ module ActionDispatch
attr_accessor :original_filename, :content_type, :tempfile, :headers
def initialize(hash)
+ @tempfile = hash[:tempfile]
+ raise(ArgumentError, ':tempfile is required') unless @tempfile
+
@original_filename = encode_filename(hash[:filename])
@content_type = hash[:type]
@headers = hash[:head]
- @tempfile = hash[:tempfile]
- raise(ArgumentError, ':tempfile is required') unless @tempfile
end
def read(*args)
@@ -16,18 +17,15 @@ module ActionDispatch
end
# Delegate these methods to the tempfile.
- [:open, :path, :rewind, :size].each do |method|
+ [:open, :path, :rewind, :size, :eof?].each do |method|
class_eval "def #{method}; @tempfile.#{method}; end"
end
-
+
private
+
def encode_filename(filename)
# Encode the filename in the utf8 encoding, unless it is nil
- if filename
- filename.force_encoding("UTF-8").encode!
- else
- filename
- end
+ filename.force_encoding("UTF-8").encode! if filename
end
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 80ffbe575b..8aa02ec482 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -23,36 +23,43 @@ module ActionDispatch
end
def url_for(options = {})
+ path = ""
+ path << options.delete(:script_name).to_s.chomp("/")
+ path << options.delete(:path).to_s
+
+ params = options[:params] || {}
+ params.reject! {|k,v| v.to_param.nil? }
+
+ result = build_host_url(options)
+
+ result << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
+ 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 build_host_url(options)
if options[:host].blank? && options[:only_path].blank?
raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
end
- rewritten_url = ""
+ result = ""
unless options[:only_path]
unless options[:protocol] == false
- rewritten_url << (options[:protocol] || "http")
- rewritten_url << ":" unless rewritten_url.match(%r{:|//})
+ result << (options[:protocol] || "http")
+ result << ":" unless result.match(%r{:|//})
end
- rewritten_url << "//" unless rewritten_url.match("//")
- rewritten_url << rewrite_authentication(options)
- rewritten_url << host_or_subdomain_and_domain(options)
- rewritten_url << ":#{options.delete(:port)}" if options[:port]
+ result << "//" unless result.match("//")
+ result << rewrite_authentication(options)
+ result << host_or_subdomain_and_domain(options)
+ result << ":#{options.delete(:port)}" if options[:port]
end
-
- path = options.delete(:path) || ''
-
- params = options[:params] || {}
- params.reject! {|k,v| v.to_param.nil? }
-
- rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
- rewritten_url << "?#{params.to_query}" unless params.empty?
- rewritten_url << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
- rewritten_url
+ result
end
- private
-
def named_host?(host)
host && IP_HOST_REGEXP !~ host
end
@@ -80,6 +87,12 @@ module ActionDispatch
end
end
+ def initialize(env)
+ super
+ @protocol = nil
+ @port = nil
+ end
+
# Returns the complete URL used for this request.
def url
protocol + host_with_port + fullpath
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index 338b116940..852f1cf6f5 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,4 +1,3 @@
-require 'active_support/core_ext/module/delegation'
module ActionDispatch
# Provide callbacks to be executed before and after the request dispatch.
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 25f1db8228..ba5d332d49 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,8 +1,8 @@
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/module/attribute_accessors'
module ActionDispatch
- class Request
+ class Request < Rack::Request
def cookie_jar
env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
end
@@ -26,9 +26,9 @@ module ActionDispatch
# # Sets a cookie that expires in 1 hour.
# cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
#
- # # Sets a signed cookie, which prevents a user from tampering with its value.
+ # # Sets a signed cookie, which prevents users from tampering with its value.
# # The cookie is signed by your app's <tt>config.secret_token</tt> value.
- # # Rails generates this value by default when you create a new Rails app.
+ # # It can be read using the signed method <tt>cookies.signed[:key]</tt>
# cookies.signed[:user_id] = current_user.id
#
# # Sets a "permanent" cookie (which expires in 20 years from now).
@@ -39,9 +39,10 @@ module ActionDispatch
#
# Examples for reading:
#
- # cookies[:user_name] # => "david"
- # cookies.size # => 2
- # cookies[:lat_lon] # => [47.68, -122.37]
+ # cookies[:user_name] # => "david"
+ # cookies.size # => 2
+ # cookies[:lat_lon] # => [47.68, -122.37]
+ # cookies.signed[:login] # => "XJ-122"
#
# Example for deleting:
#
@@ -82,7 +83,7 @@ module ActionDispatch
TOKEN_KEY = "action_dispatch.secret_token".freeze
# Raised when storing more than 4K of session data.
- class CookieOverflow < StandardError; end
+ CookieOverflow = Class.new StandardError
class CookieJar #:nodoc:
include Enumerable
@@ -117,7 +118,6 @@ module ActionDispatch
@delete_cookies = {}
@host = host
@secure = secure
- @closed = false
@cookies = {}
end
@@ -154,7 +154,7 @@ module ActionDispatch
end
elsif options[:domain].is_a? Array
# if host matches one of the supplied domains without a dot in front of it
- options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] }
+ options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') }
end
end
@@ -169,12 +169,14 @@ module ActionDispatch
options = { :value => value }
end
- @cookies[key.to_s] = value
-
handle_options(options)
- @set_cookies[key.to_s] = options
- @delete_cookies.delete(key.to_s)
+ if @cookies[key.to_s] != value or options[:expires]
+ @cookies[key.to_s] = value
+ @set_cookies[key.to_s] = options
+ @delete_cookies.delete(key.to_s)
+ end
+
value
end
@@ -182,8 +184,9 @@ module ActionDispatch
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
def delete(key, options = {})
- options.symbolize_keys!
+ return unless @cookies.has_key? key.to_s
+ options.symbolize_keys!
handle_options(options)
value = @cookies.delete(key.to_s)
@@ -225,7 +228,7 @@ module ActionDispatch
# cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
# be raised.
#
- # This jar requires that you set a suitable secret for the verification on your app's config.secret_token.
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+.
#
# Example:
#
@@ -273,10 +276,6 @@ module ActionDispatch
@parent_jar[key] = options
end
- def signed
- @signed ||= SignedCookieJar.new(self, @secret)
- end
-
def method_missing(method, *arguments, &block)
@parent_jar.send(method, *arguments, &block)
end
@@ -343,7 +342,6 @@ module ActionDispatch
end
def call(env)
- cookie_jar = nil
status, headers, body = @app.call(env)
if cookie_jar = env['action_dispatch.cookies']
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index b903f98761..0f0589a844 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,5 +1,7 @@
require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
+require 'action_dispatch/routing/inspector'
+
module ActionDispatch
# This middleware is responsible for logging exceptions and
@@ -7,8 +9,9 @@ module ActionDispatch
class DebugExceptions
RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
- def initialize(app)
- @app = app
+ def initialize(app, routes_app = nil)
+ @app = app
+ @routes_app = routes_app
end
def call(env)
@@ -39,7 +42,8 @@ module ActionDispatch
:exception => wrapper.exception,
:application_trace => wrapper.application_trace,
:framework_trace => wrapper.framework_trace,
- :full_trace => wrapper.full_trace
+ :full_trace => wrapper.full_trace,
+ :routes => formatted_routes(exception)
)
file = "rescues/#{wrapper.rescue_template}"
@@ -78,5 +82,13 @@ module ActionDispatch
def stderr_logger
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
end
+
+ def formatted_routes(exception)
+ return false unless @routes_app.respond_to?(:routes)
+ if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)
+ inspector = ActionDispatch::Routing::RoutesInspector.new
+ inspector.format(@routes_app.routes.routes).join("\n")
+ end
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index c0532c80c4..ae38c56a67 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,5 +1,6 @@
require 'action_controller/metal/exceptions'
require 'active_support/core_ext/exception'
+require 'active_support/core_ext/class/attribute_accessors'
module ActionDispatch
class ExceptionWrapper
@@ -10,7 +11,9 @@ module ActionDispatch
'AbstractController::ActionNotFound' => :not_found,
'ActionController::MethodNotAllowed' => :method_not_allowed,
'ActionController::NotImplemented' => :not_implemented,
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
+ 'ActionController::UnknownFormat' => :not_acceptable,
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
+ 'ActionController::BadRequest' => :bad_request
)
cattr_accessor :rescue_templates
@@ -34,7 +37,7 @@ module ActionDispatch
end
def status_code
- Rack::Utils.status_code(@@rescue_responses[@exception.class.name])
+ self.class.status_code_for_exception(@exception.class.name)
end
def application_trace
@@ -49,6 +52,10 @@ module ActionDispatch
clean_backtrace(:all)
end
+ def self.status_code_for_exception(class_name)
+ Rack::Utils.status_code(@@rescue_responses[class_name])
+ end
+
private
def original_exception(exception)
@@ -75,4 +82,4 @@ module ActionDispatch
@backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner']
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index cff0877030..9928b7cc3a 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,23 +1,23 @@
module ActionDispatch
- class Request
+ class Request < Rack::Request
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash
- @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new)
+ @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new).tap(&:sweep)
end
end
# The flash provides a way to pass temporary objects 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. Example:
+ # then expose the flash to its template. Actually, that exposure is automatically done.
#
# class PostsController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Post successfully created"
- # redirect_to posts_path(@post)
+ # redirect_to @post
# end
#
# def show
@@ -79,7 +79,6 @@ module ActionDispatch
def initialize #:nodoc:
@discard = Set.new
- @closed = false
@flashes = {}
@now = nil
end
@@ -217,13 +216,9 @@ module ActionDispatch
end
def call(env)
- if (session = env['rack.session']) && (flash = session['flash'])
- flash.sweep
- end
-
@app.call(env)
ensure
- session = env['rack.session'] || {}
+ session = Request::Session.find(env) || {}
flash_hash = env[KEY]
if flash_hash
@@ -237,7 +232,8 @@ module ActionDispatch
env[KEY] = new_hash
end
- if session.key?('flash') && session['flash'].empty?
+ if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
+ session.key?('flash') && session['flash'].empty?
session.delete('flash')
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/head.rb b/actionpack/lib/action_dispatch/middleware/head.rb
deleted file mode 100644
index f1906a3ab3..0000000000
--- a/actionpack/lib/action_dispatch/middleware/head.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActionDispatch
- class Head
- def initialize(app)
- @app = app
- end
-
- def call(env)
- if env["REQUEST_METHOD"] == "HEAD"
- env["REQUEST_METHOD"] = "GET"
- env["rack.methodoverride.original_method"] = "HEAD"
- status, headers, _ = @app.call(env)
- [status, headers, []]
- else
- @app.call(env)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 85b8d178bf..53bedaa40a 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -1,5 +1,4 @@
module ActionDispatch
- # A simple Rack application that renders exceptions in the given public path.
class PublicExceptions
attr_accessor :public_path
@@ -8,23 +7,41 @@ module ActionDispatch
end
def call(env)
- status = env["PATH_INFO"][1..-1]
- locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
- path = "#{public_path}/#{status}.html"
-
- if locale_path && File.exist?(locale_path)
- render(status, File.read(locale_path))
- elsif File.exist?(path)
- render(status, File.read(path))
+ exception = env["action_dispatch.exception"]
+ status = env["PATH_INFO"][1..-1]
+ request = ActionDispatch::Request.new(env)
+ content_type = request.formats.first
+ body = { :status => status, :error => exception.message }
+
+ render(status, content_type, body)
+ end
+
+ private
+
+ def render(status, content_type, body)
+ format = content_type && "to_#{content_type.to_sym}"
+ if format && body.respond_to?(format)
+ render_format(status, content_type, body.public_send(format))
else
- [404, { "X-Cascade" => "pass" }, []]
+ render_html(status)
end
end
- private
+ def render_format(status, content_type, body)
+ [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
+ 'Content-Length' => body.bytesize.to_s}, [body]]
+ 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))
- def render(status, body)
- [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
+ if found || File.exist?(path)
+ render_format(status, 'text/html', File.read(path))
+ else
+ [404, { "X-Cascade" => "pass" }, []]
+ end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index a0388e0e13..2f6968eb2e 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -18,10 +18,10 @@ module ActionDispatch
# classes before they are unloaded.
#
# By default, ActionDispatch::Reloader is included in the middleware stack
- # only in the development environment; specifically, when config.cache_classes
+ # only in the development environment; specifically, when +config.cache_classes+
# is false. Callbacks may be registered even when it is not included in the
- # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+
- # or +ActionDispatch::Reloader.cleanup!+ are called manually.
+ # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
+ # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
#
class Reloader
include ActiveSupport::Callbacks
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index d924f21fad..ec15a2a715 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -5,11 +5,14 @@ module ActionDispatch
# IP addresses that are "trusted proxies" that can be stripped from
# the comma-delimited list in the X-Forwarded-For header. See also:
# http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
+ # http://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses.
TRUSTED_PROXIES = %r{
^127\.0\.0\.1$ | # localhost
+ ^::1$ |
^(10 | # private IP 10.x.x.x
172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255
- 192\.168 # private IP 192.168.x.x
+ 192\.168 | # private IP 192.168.x.x
+ fc00:: # private IP fc00
)\.
}x
@@ -19,13 +22,13 @@ module ActionDispatch
@app = app
@check_ip = check_ip_spoofing
@proxies = case custom_proxies
- when Regexp
- custom_proxies
- when nil
- TRUSTED_PROXIES
- else
- Regexp.union(TRUSTED_PROXIES, custom_proxies)
- end
+ when Regexp
+ custom_proxies
+ when nil
+ TRUSTED_PROXIES
+ else
+ Regexp.union(TRUSTED_PROXIES, custom_proxies)
+ end
end
def call(env)
@@ -34,6 +37,31 @@ module ActionDispatch
end
class GetIp
+
+ # IP v4 and v6 (with compression) validation regexp
+ # https://gist.github.com/1289635
+ VALID_IP = %r{
+ (^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
+ (^(
+ (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
+ (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
+ (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
+ (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
+ (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
+ (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
+ (([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ (([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ (([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ (([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ (([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
+ ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
+ (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining
+ (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
+ )$)
+ }x
+
def initialize(env, middleware)
@env = env
@middleware = middleware
@@ -44,25 +72,31 @@ module ActionDispatch
# but will be wrong if the user is behind a proxy. Proxies will set
# HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those.
# HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of
- # multiple chained proxies. The last address which is not a known proxy
- # will be the originating IP.
+ # multiple chained proxies. The first address which is in this list
+ # if it's not a known proxy will be the originating IP.
+ # Format of HTTP_X_FORWARDED_FOR:
+ # client_ip, proxy_ip1, proxy_ip2...
+ # http://en.wikipedia.org/wiki/X-Forwarded-For
def calculate_ip
- client_ip = @env['HTTP_CLIENT_IP']
- forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR')
- remote_addrs = ips_from('REMOTE_ADDR')
+ client_ip = @env['HTTP_CLIENT_IP']
+ forwarded_ip = ips_from('HTTP_X_FORWARDED_FOR').first
+ remote_addrs = ips_from('REMOTE_ADDR')
check_ip = client_ip && @middleware.check_ip
- if check_ip && !forwarded_ips.include?(client_ip)
+ if check_ip && forwarded_ip != 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
- not_proxy = client_ip || forwarded_ips.first || remote_addrs.first
-
- # Return first REMOTE_ADDR if there are no other options
- not_proxy || ips_from('REMOTE_ADDR', :allow_proxies).first
+ client_ips = remove_proxies [client_ip, forwarded_ip, remote_addrs].flatten
+ if client_ips.present?
+ client_ips.first
+ else
+ # If there is no client ip we can return first valid proxy ip from REMOTE_ADDR
+ remote_addrs.find { |ip| valid_ip? ip }
+ end
end
def to_s
@@ -71,12 +105,24 @@ module ActionDispatch
@ip = calculate_ip
end
- protected
+ private
- def ips_from(header, allow_proxies = false)
- ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
- allow_proxies ? ips : ips.reject{|ip| ip =~ @middleware.proxies }
+ def ips_from(header)
+ @env[header] ? @env[header].strip.split(/[,\s]+/) : []
end
+
+ def valid_ip?(ip)
+ ip =~ VALID_IP
+ end
+
+ def not_a_proxy?(ip)
+ ip !~ @middleware.proxies
+ end
+
+ def remove_proxies(ips)
+ ips.select { |ip| valid_ip?(ip) && not_a_proxy?(ip) }
+ end
+
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index d5a0b80fd5..44290445d4 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -1,6 +1,5 @@
require 'securerandom'
require 'active_support/core_ext/string/access'
-require 'active_support/core_ext/object/blank'
module ActionDispatch
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
@@ -19,10 +18,7 @@ module ActionDispatch
def call(env)
env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
- status, headers, body = @app.call(env)
-
- headers["X-Request-Id"] = env["action_dispatch.request_id"]
- [ status, headers, body ]
+ @app.call(env).tap { |status, headers, body| headers["X-Request-Id"] = env["action_dispatch.request_id"] }
end
private
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 6a8e690d18..7c12590c49 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -2,26 +2,22 @@ require 'rack/utils'
require 'rack/request'
require 'rack/session/abstract/id'
require 'action_dispatch/middleware/cookies'
-require 'active_support/core_ext/object/blank'
+require 'action_dispatch/request/session'
module ActionDispatch
module Session
class SessionRestoreError < StandardError #:nodoc:
- end
+ attr_reader :original_exception
+
+ def initialize(const_error)
+ @original_exception = const_error
- module DestroyableSession
- def destroy
- clear
- options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env
- options ||= {}
- @by.send(:destroy_session, @env, options[:id], options) if @by
- options[:id] = nil
- @loaded = false
+ super("Session contains objects whose class definition isn't available.\n" +
+ "Remember to require the classes for all objects kept in the session.\n" +
+ "(Original exception: #{const_error.message} [#{const_error.class}])\n")
end
end
- ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession
-
module Compatibility
def initialize(app, options = {})
options[:key] ||= '_session_id'
@@ -58,11 +54,8 @@ module ActionDispatch
begin
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
- rescue LoadError, NameError => const_error
- raise ActionDispatch::Session::SessionRestoreError,
- "Session contains objects whose class definition isn't available.\n" +
- "Remember to require the classes for all objects kept in the session.\n" +
- "(Original exception: #{const_error.message} [#{const_error.class}])\n"
+ rescue LoadError, NameError => e
+ raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace
end
retry
else
@@ -71,9 +64,27 @@ module ActionDispatch
end
end
+ module SessionObject # :nodoc:
+ def prepare_session(env)
+ Request::Session.create(self, env, @default_options)
+ end
+
+ def loaded_session?(session)
+ !session.is_a?(Request::Session) || session.loaded?
+ end
+ end
+
class AbstractStore < Rack::Session::Abstract::ID
include Compatibility
include StaleSessionCheck
+ include SessionObject
+
+ private
+
+ def set_cookie(env, session_id, cookie)
+ request = ActionDispatch::Request.new(env)
+ request.cookie_jar[key] = cookie
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index d3b6fd12fa..1db6194271 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -1,5 +1,4 @@
require 'action_dispatch/middleware/session/abstract_store'
-require 'rack/session/memcache'
module ActionDispatch
module Session
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 8ebf870b95..019849ef95 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 'active_support/core_ext/object/blank'
require 'action_dispatch/middleware/session/abstract_store'
require 'rack/session/cookie'
@@ -27,7 +26,7 @@ module ActionDispatch
# CGI::Session instance as an argument. It's important that the secret
# is not vulnerable to a dictionary attack. Therefore, you should choose
# a secret consisting of random numbers and letters and more than 30
- # characters. Examples:
+ # characters.
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
@@ -43,6 +42,15 @@ module ActionDispatch
class CookieStore < Rack::Session::Cookie
include Compatibility
include StaleSessionCheck
+ include SessionObject
+
+ # Override rack's method
+ def destroy_session(env, session_id, options)
+ new_sid = super
+ # Reset hash and Assign the new session id
+ env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
+ new_sid
+ end
private
@@ -59,7 +67,8 @@ module ActionDispatch
end
def set_session(env, sid, session_data, options)
- session_data.merge("session_id" => sid)
+ session_data["session_id"] = sid
+ session_data
end
def set_cookie(env, session_id, cookie)
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
index 4dd9a946c2..b4d6629c35 100644
--- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -1,14 +1,19 @@
require 'action_dispatch/middleware/session/abstract_store'
-require 'rack/session/memcache'
+begin
+ require 'rack/session/dalli'
+rescue LoadError => e
+ $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
+ raise e
+end
module ActionDispatch
module Session
- class MemCacheStore < Rack::Session::Memcache
+ class MemCacheStore < Rack::Session::Dalli
include Compatibility
include StaleSessionCheck
+ include SessionObject
def initialize(app, options = {})
- require 'memcache'
options[:expire_after] ||= options[:expires]
super
end
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 836136eb95..ab740a0190 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -9,7 +9,7 @@ module ActionDispatch
# of ShowExceptions. Everytime there is an exception, ShowExceptions will
# store the exception in env["action_dispatch.exception"], rewrite the
# PATH_INFO to the exception status code and call the rack app.
- #
+ #
# If the application returns a "X-Cascade" pass response, this middleware
# will send an empty response as result with the correct status code.
# If any exception happens inside the exceptions app, this middleware
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
new file mode 100644
index 0000000000..9098f4e170
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -0,0 +1,70 @@
+module ActionDispatch
+ class SSL
+ YEAR = 31536000
+
+ def self.default_hsts_options
+ { :expires => YEAR, :subdomains => false }
+ end
+
+ def initialize(app, options = {})
+ @app = app
+
+ @hsts = options.fetch(:hsts, {})
+ @hsts = {} if @hsts == true
+ @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
+
+ @host = options[:host]
+ @port = options[:port]
+ end
+
+ def call(env)
+ request = Request.new(env)
+
+ if request.ssl?
+ status, headers, body = @app.call(env)
+ headers = hsts_headers.merge(headers)
+ flag_cookies_as_secure!(headers)
+ [status, headers, body]
+ else
+ redirect_to_https(request)
+ end
+ end
+
+ private
+ def redirect_to_https(request)
+ url = URI(request.url)
+ url.scheme = "https"
+ url.host = @host if @host
+ url.port = @port if @port
+ headers = hsts_headers.merge('Content-Type' => 'text/html',
+ 'Location' => url.to_s)
+
+ [301, headers, []]
+ end
+
+ # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
+ def hsts_headers
+ if @hsts
+ value = "max-age=#{@hsts[:expires]}"
+ value += "; includeSubDomains" if @hsts[:subdomains]
+ { 'Strict-Transport-Security' => value }
+ else
+ {}
+ end
+ end
+
+ def flag_cookies_as_secure!(headers)
+ if cookies = headers['Set-Cookie']
+ cookies = cookies.split("\n")
+
+ headers['Set-Cookie'] = cookies.map { |cookie|
+ if cookie !~ /;\s+secure(;|$)/
+ "#{cookie}; secure"
+ else
+ cookie
+ end
+ }.join("\n")
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index 28e8fbdab8..bbf734f103 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -75,6 +75,11 @@ module ActionDispatch
middlewares[i]
end
+ def unshift(*args, &block)
+ middleware = self.class::Middleware.new(*args, &block)
+ middlewares.unshift(middleware)
+ end
+
def initialize_copy(other)
self.middlewares = other.middlewares.dup
end
@@ -110,7 +115,7 @@ module ActionDispatch
def build(app = nil, &block)
app ||= block
raise "MiddlewareStack#build requires an app" unless app
- middlewares.reverse.inject(app) { |a, e| e.build(a) }
+ middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
end
protected
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 63b7422287..9073e6582d 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -39,6 +39,7 @@ module ActionDispatch
end
def escape_glob_chars(path)
+ path.force_encoding('binary') if path.respond_to? :force_encoding
path.gsub(/[*?{}\[\]]/, "\\\\\\&")
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
index 0c5bafa666..823f5d25b6 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
@@ -12,8 +12,8 @@
request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
- def debug_hash(hash)
- hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
end unless self.class.method_defined?(:debug_hash)
%>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
index f06c07daa5..8c594c1523 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
@@ -10,6 +10,14 @@
</ol>
</p>
<% end %>
+<%= render :template => "rescues/_trace" %>
+
+<h2>
+ Routes
+</h2>
+
<p>
- Try running <code>rake routes</code> for more information on available routes.
-</p> \ No newline at end of file
+ Routes match in priority from top to bottom
+</p>
+
+<p><pre><%= @routes %></pre></p>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 4135f3c142..ccc0435a39 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -16,14 +16,22 @@ module ActionDispatch
config.action_dispatch.rack_cache = {
:metastore => "rails:/",
:entitystore => "rails:/",
- :verbose => true
+ :verbose => false
}
+ config.action_dispatch.default_headers = {
+ 'X-Frame-Options' => 'SAMEORIGIN',
+ 'X-XSS-Protection' => '1; mode=block',
+ 'X-Content-Type-Options' => 'nosniff'
+ }
+
+ config.eager_load_namespaces << ActionDispatch
+
initializer "action_dispatch.configure" do |app|
ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
- ActionDispatch::Routing::Mapper.default_method_for_update = app.config.default_method_for_update
+ ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers
ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
new file mode 100644
index 0000000000..a05a23d953
--- /dev/null
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -0,0 +1,177 @@
+require 'rack/session/abstract/id'
+
+module ActionDispatch
+ class Request < Rack::Request
+ # Session is responsible for lazily loading the session from store.
+ class Session # :nodoc:
+ ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc:
+ ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc:
+
+ def self.create(store, env, default_options)
+ session_was = find env
+ session = Request::Session.new(store, env)
+ session.merge! session_was if session_was
+
+ set(env, session)
+ Options.set(env, Request::Session::Options.new(store, env, default_options))
+ session
+ end
+
+ def self.find(env)
+ env[ENV_SESSION_KEY]
+ end
+
+ def self.set(env, session)
+ env[ENV_SESSION_KEY] = session
+ end
+
+ class Options #:nodoc:
+ def self.set(env, options)
+ env[ENV_SESSION_OPTIONS_KEY] = options
+ end
+
+ def self.find(env)
+ env[ENV_SESSION_OPTIONS_KEY]
+ end
+
+ def initialize(by, env, default_options)
+ @by = by
+ @env = env
+ @delegate = default_options.dup
+ end
+
+ def [](key)
+ if key == :id
+ @delegate.fetch(key) {
+ @delegate[:id] = @by.send(:extract_session_id, @env)
+ }
+ else
+ @delegate[key]
+ end
+ end
+
+ def []=(k,v); @delegate[k] = v; end
+ def to_hash; @delegate.dup; end
+ def values_at(*args); @delegate.values_at(*args); end
+ end
+
+ def initialize(by, env)
+ @by = by
+ @env = env
+ @delegate = {}
+ @loaded = false
+ @exists = nil # we haven't checked yet
+ end
+
+ def options
+ Options.find @env
+ end
+
+ def destroy
+ clear
+ options = self.options || {}
+ new_sid = @by.send(:destroy_session, @env, options[:id], options)
+ options[:id] = new_sid # Reset session id with a new value or nil
+
+ # Load the new sid to be written with the response
+ @loaded = false
+ load_for_write!
+ end
+
+ def [](key)
+ load_for_read!
+ @delegate[key.to_s]
+ end
+
+ def has_key?(key)
+ load_for_read!
+ @delegate.key?(key.to_s)
+ end
+ alias :key? :has_key?
+ alias :include? :has_key?
+
+ def keys
+ @delegate.keys
+ end
+
+ def values
+ @delegate.values
+ end
+
+ def []=(key, value)
+ load_for_write!
+ @delegate[key.to_s] = value
+ end
+
+ def clear
+ load_for_write!
+ @delegate.clear
+ end
+
+ def to_hash
+ load_for_read!
+ @delegate.dup.delete_if { |_,v| v.nil? }
+ end
+
+ def update(hash)
+ load_for_write!
+ @delegate.update stringify_keys(hash)
+ end
+
+ def delete(key)
+ load_for_write!
+ @delegate.delete key.to_s
+ end
+
+ def inspect
+ if loaded?
+ super
+ else
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>"
+ end
+ end
+
+ def exists?
+ return @exists unless @exists.nil?
+ @exists = @by.send(:session_exists?, @env)
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ def empty?
+ load_for_read!
+ @delegate.empty?
+ end
+
+ def merge!(other)
+ load_for_write!
+ @delegate.merge!(other)
+ end
+
+ private
+
+ def load_for_read!
+ load! if !loaded? && exists?
+ end
+
+ def load_for_write!
+ load! unless loaded?
+ end
+
+ def load!
+ id, session = @by.load_session @env
+ options[:id] = id
+ @delegate.replace(stringify_keys(session))
+ @loaded = true
+ end
+
+ def stringify_keys(other)
+ other.each_with_object({}) { |(key, value), hash|
+ hash[key.to_s] = value
+ }
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 38a0270151..29090882a5 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,3 +1,4 @@
+# encoding: UTF-8
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/regexp'
@@ -218,6 +219,12 @@ module ActionDispatch
#
# match "/stories" => redirect("/posts")
#
+ # == Unicode character routes
+ #
+ # You can specify unicode character routes in your router:
+ #
+ # match "こんにちは" => "welcome#index"
+ #
# == Routing to Rack Applications
#
# Instead of a String, like <tt>posts#index</tt>, which corresponds to the
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
new file mode 100644
index 0000000000..bc7229b6a1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -0,0 +1,121 @@
+require 'delegate'
+
+module ActionDispatch
+ module Routing
+ class RouteWrapper < SimpleDelegator
+ def endpoint
+ rack_app ? rack_app.inspect : "#{controller}##{action}"
+ 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
+ end
+
+ def verb
+ super.source.gsub(/[$^]/, '')
+ end
+
+ def path
+ super.spec.to_s
+ end
+
+ def name
+ super.to_s
+ end
+
+ def reqs
+ @reqs ||= begin
+ reqs = endpoint
+ reqs += " #{constraints.inspect}" unless constraints.empty?
+ reqs
+ end
+ end
+
+ def controller
+ requirements[:controller] || ':controller'
+ end
+
+ def action
+ requirements[:action] || ':action'
+ end
+
+ def internal?
+ path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}}
+ end
+
+ def engine?
+ rack_app && rack_app.respond_to?(:routes)
+ end
+ end
+
+ ##
+ # This class is just used for displaying route information when someone
+ # executes `rake routes`. People should not use this class.
+ class RoutesInspector # :nodoc:
+ def initialize
+ @engines = Hash.new
+ end
+
+ def format(all_routes, filter = nil)
+ if filter
+ all_routes = all_routes.select{ |route| route.defaults[:controller] == filter }
+ end
+
+ routes = collect_routes(all_routes)
+
+ formatted_routes(routes) +
+ formatted_routes_for_engines
+ end
+
+ def collect_routes(routes)
+ routes = routes.collect do |route|
+ RouteWrapper.new(route)
+ end.reject do |route|
+ route.internal?
+ end.collect do |route|
+ collect_engine_routes(route)
+
+ {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs }
+ end
+ end
+
+ def collect_engine_routes(route)
+ name = route.endpoint
+ return unless route.engine?
+ return if @engines[name]
+
+ routes = route.rack_app.routes
+ if routes.is_a?(ActionDispatch::Routing::RouteSet)
+ @engines[name] = collect_routes(routes.routes)
+ end
+ end
+
+ def formatted_routes_for_engines
+ @engines.map do |name, routes|
+ ["\nRoutes for #{name}:"] + formatted_routes(routes)
+ end.flatten
+ end
+
+ def formatted_routes(routes)
+ name_width = routes.map{ |r| r[:name].length }.max
+ verb_width = routes.map{ |r| r[:verb].length }.max
+ path_width = routes.map{ |r| r[:path].length }.max
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index f66d28bf83..49afa01d25 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,14 +1,13 @@
require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/inclusion'
+require 'active_support/core_ext/hash/reverse_merge'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/enumerable'
require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
module ActionDispatch
module Routing
class Mapper
- cattr_accessor(:default_method_for_update) {:put}
-
class Constraints #:nodoc:
def self.new(app, constraints, request = Rack::Request)
if constraints.any?
@@ -36,6 +35,8 @@ module ActionDispatch
}
return true
+ ensure
+ req.reset_parameters
end
def call(env)
@@ -60,6 +61,16 @@ module ActionDispatch
@options = (@scope[:options] || {}).merge(options)
@path = normalize_path(path)
normalize_options!
+
+ via_all = @options.delete(:via) if @options[:via] == :all
+
+ if !via_all && request_method_condition.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 GET, use `get` in the router:\n\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise msg
+ end
end
def to_route
@@ -89,6 +100,10 @@ module ActionDispatch
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
end
end
+
+ if @options[:constraints].is_a?(Hash)
+ (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints]))
+ end
end
# match "account/overview"
@@ -167,7 +182,7 @@ module ActionDispatch
controller ||= default_controller
action ||= default_action
- unless controller.is_a?(Regexp) || to_shorthand
+ unless controller.is_a?(Regexp)
controller = [@scope[:module], controller].compact.join("/").presence
end
@@ -234,6 +249,11 @@ module ActionDispatch
def default_action
@options[:action] || @scope[:action]
end
+
+ def defaults_from_constraints(constraints)
+ url_keys = [:protocol, :subdomain, :domain, :host, :port]
+ constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) }
+ end
end
# Invokes Rack::Mount::Utils.normalize path and ensure that
@@ -241,12 +261,12 @@ module ActionDispatch
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
path = Journey::Router::Utils.normalize_path(path)
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$}
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
path
end
def self.normalize_name(name)
- normalize_path(name)[1..-1].gsub("/", "_")
+ normalize_path(name)[1..-1].tr("/", "_")
end
module Base
@@ -256,11 +276,16 @@ module ActionDispatch
#
# For options, see +match+, as +root+ uses it internally.
#
+ # You can also pass a string which will expand
+ #
+ # root 'pages#main'
+ #
# You should put the root route at the top of <tt>config/routes.rb</tt>,
# because this means it will be matched first. As this is the most popular route
# of most Rails applications, this is beneficial.
def root(options = {})
- match '/', { :as => :root }.merge(options)
+ options = { :to => options } if options.is_a?(String)
+ match '/', { :as => :root, :via => :get }.merge(options)
end
# Matches a url pattern to one or more routes. Any symbols in a pattern
@@ -330,7 +355,7 @@ module ActionDispatch
# +call+ or a string representing a controller's action.
#
# match 'path', :to => 'controller#action'
- # match 'path', :to => lambda { [200, {}, "Success!"] }
+ # match 'path', :to => lambda { |env| [200, {}, "Success!"] }
# match 'path', :to => RackApp
#
# [:on]
@@ -378,6 +403,10 @@ module ActionDispatch
#
# # Matches any request starting with 'path'
# match 'path' => 'c#a', :anchor => false
+ #
+ # [:format]
+ # Allows you to specify the default value for optional +format+
+ # segment or disable it by supplying +false+.
def match(path, options=nil)
end
@@ -404,6 +433,10 @@ module ActionDispatch
if options
path = options.delete(:at)
else
+ unless Hash === app
+ raise ArgumentError, "must be called with mount point"
+ end
+
options = app
app, path = options.find { |k, v| k.respond_to?(:call) }
options.delete(app) if app
@@ -411,7 +444,8 @@ module ActionDispatch
raise "A rack application must be specified" unless path
- options[:as] ||= app_name(app)
+ options[:as] ||= app_name(app)
+ options[:via] ||= :all
match(path, options.merge(:to => app, :anchor => false, :format => false))
@@ -438,7 +472,7 @@ module ActionDispatch
app.railtie_name
else
class_name = app.class.is_a?(Class) ? app.name : app.class.name
- ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
end
end
@@ -448,7 +482,11 @@ module ActionDispatch
_route = @set.named_routes.routes[name.to_sym]
_routes = @set
app.routes.define_mounted_helper(name)
- app.routes.class_eval do
+ app.routes.singleton_class.class_eval do
+ define_method :mounted? do
+ true
+ end
+
define_method :_generate_prefix do |options|
prefix_options = options.slice(*_route.segment_keys)
# we must actually delete prefix segment keys to avoid passing them to next url_for
@@ -465,8 +503,6 @@ module ActionDispatch
# Define a route that only recognizes HTTP GET.
# For supported arguments, see <tt>Base#match</tt>.
#
- # Example:
- #
# get 'bacon', :to => 'food#bacon'
def get(*args, &block)
map_method(:get, args, &block)
@@ -475,8 +511,6 @@ module ActionDispatch
# Define a route that only recognizes HTTP POST.
# For supported arguments, see <tt>Base#match</tt>.
#
- # Example:
- #
# post 'bacon', :to => 'food#bacon'
def post(*args, &block)
map_method(:post, args, &block)
@@ -485,8 +519,6 @@ module ActionDispatch
# Define a route that only recognizes HTTP PATCH.
# For supported arguments, see <tt>Base#match</tt>.
#
- # Example:
- #
# patch 'bacon', :to => 'food#bacon'
def patch(*args, &block)
map_method(:patch, args, &block)
@@ -495,8 +527,6 @@ module ActionDispatch
# Define a route that only recognizes HTTP PUT.
# For supported arguments, see <tt>Base#match</tt>.
#
- # Example:
- #
# put 'bacon', :to => 'food#bacon'
def put(*args, &block)
map_method(:put, args, &block)
@@ -505,8 +535,6 @@ module ActionDispatch
# Define a route that only recognizes HTTP DELETE.
# For supported arguments, see <tt>Base#match</tt>.
#
- # Example:
- #
# delete 'broccoli', :to => 'food#broccoli'
def delete(*args, &block)
map_method(:delete, args, &block)
@@ -515,7 +543,8 @@ module ActionDispatch
private
def map_method(method, args, &block)
options = args.extract_options!
- options[:via] = method
+ options[:via] = method
+ options[:path] ||= args.first if args.first.is_a?(String)
match(*args, options, &block)
self
end
@@ -539,7 +568,7 @@ module ActionDispatch
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
- # PUT/PATCH /admin/posts/1
+ # PATCH/PUT /admin/posts/1
# DELETE /admin/posts/1
#
# If you want to route /posts (without the prefix /admin) to
@@ -573,7 +602,7 @@ module ActionDispatch
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
- # PUT/PATCH /admin/posts/1
+ # PATCH/PUT /admin/posts/1
# DELETE /admin/posts/1
module Scoping
# Scopes a set of routes to the given default options.
@@ -620,6 +649,10 @@ module ActionDispatch
block, options[:constraints] = options[:constraints], {}
end
+ if options[:constraints].is_a?(Hash)
+ (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints]))
+ end
+
scope_options.each do |option|
if value = options.delete(option)
recover[option] = @scope[option]
@@ -646,7 +679,6 @@ module ActionDispatch
# Scopes routes to a specific controller
#
- # Example:
# controller "food" do
# match "bacon", :action => "bacon"
# end
@@ -668,7 +700,7 @@ module ActionDispatch
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
- # admin_post PUT/PATCH /admin/posts/:id(.:format) admin/posts#update
+ # admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
#
# === Options
@@ -725,7 +757,7 @@ module ActionDispatch
#
# Routes can also be constrained to an IP or a certain range of IP addresses:
#
- # constraints(:ip => /192.168.\d+.\d+/) do
+ # constraints(:ip => /192\.168\.\d+\.\d+/) do
# resources :posts
# end
#
@@ -828,6 +860,11 @@ module ActionDispatch
def override_keys(child) #:nodoc:
child.key?(:only) || child.key?(:except) ? [:only, :except] : []
end
+
+ def defaults_from_constraints(constraints)
+ url_keys = [:protocol, :subdomain, :domain, :host, :port]
+ constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) }
+ end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -873,17 +910,18 @@ module ActionDispatch
# CANONICAL_ACTIONS holds all actions that does not need a prefix or
# a path appended since they fit properly in their scope level.
VALID_ON_OPTIONS = [:new, :collection, :member]
- RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except]
+ RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
class Resource #:nodoc:
- attr_reader :controller, :path, :options
+ attr_reader :controller, :path, :options, :param
def initialize(entities, options = {})
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@as = options[:as]
+ @param = (options[:param] || :id).to_sym
@options = options
end
@@ -928,15 +966,21 @@ module ActionDispatch
alias :collection_scope :path
def member_scope
- "#{path}/:id"
+ "#{path}/:#{param}"
end
+ alias :shallow_scope :member_scope
+
def new_scope(new_path)
"#{path}/#{new_path}"
end
+ def nested_param
+ :"#{singular}_#{param}"
+ end
+
def nested_scope
- "#{path}/:#{singular}_id"
+ "#{path}/:#{nested_param}"
end
end
@@ -988,7 +1032,7 @@ module ActionDispatch
# POST /geocoder
# GET /geocoder
# GET /geocoder/edit
- # PUT/PATCH /geocoder
+ # PATCH/PUT /geocoder
# DELETE /geocoder
#
# === Options
@@ -1003,6 +1047,8 @@ module ActionDispatch
resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
yield if block_given?
+ concerns(options[:concerns]) if options[:concerns]
+
collection do
post :create
end if parent_resource.actions.include?(:create)
@@ -1012,10 +1058,11 @@ module ActionDispatch
end if parent_resource.actions.include?(:new)
member do
- get :edit if parent_resource.actions.include?(:edit)
- get :show if parent_resource.actions.include?(:show)
+ get :edit if parent_resource.actions.include?(:edit)
+ get :show if parent_resource.actions.include?(:show)
if parent_resource.actions.include?(:update)
- send default_method_for_update, :update
+ patch :update
+ put :update
end
delete :destroy if parent_resource.actions.include?(:destroy)
end
@@ -1039,7 +1086,7 @@ module ActionDispatch
# POST /photos
# GET /photos/:id
# GET /photos/:id/edit
- # PUT/PATCH /photos/:id
+ # PATCH/PUT /photos/:id
# DELETE /photos/:id
#
# Resources can also be nested infinitely by using this block syntax:
@@ -1055,7 +1102,7 @@ module ActionDispatch
# POST /photos/:photo_id/comments
# GET /photos/:photo_id/comments/:id
# GET /photos/:photo_id/comments/:id/edit
- # PUT/PATCH /photos/:photo_id/comments/:id
+ # PATCH/PUT /photos/:photo_id/comments/:id
# DELETE /photos/:photo_id/comments/:id
#
# === Options
@@ -1123,9 +1170,32 @@ module ActionDispatch
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
- # comment PUT/PATCH /sekret/comments/:id(.:format)
+ # comment PATCH/PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
#
+ # [:shallow_prefix]
+ # Prefixes nested shallow route names with specified prefix.
+ #
+ # scope :shallow_prefix => "sekret" do
+ # resources :posts do
+ # resources :comments, :shallow => true
+ # end
+ # end
+ #
+ # The +comments+ resource here will have the following routes generated for it:
+ #
+ # post_comments GET /posts/:post_id/comments(.:format)
+ # post_comments POST /posts/:post_id/comments(.:format)
+ # new_post_comment GET /posts/:post_id/comments/new(.:format)
+ # edit_sekret_comment GET /comments/:id/edit(.:format)
+ # sekret_comment GET /comments/:id(.:format)
+ # sekret_comment PATCH/PUT /comments/:id(.:format)
+ # sekret_comment DELETE /comments/:id(.:format)
+ #
+ # [:format]
+ # Allows you to specify the default value for optional +format+
+ # segment or disable it by supplying +false+.
+ #
# === Examples
#
# # routes call <tt>Admin::PostsController</tt>
@@ -1143,6 +1213,8 @@ module ActionDispatch
resource_scope(:resources, Resource.new(resources.pop, options)) do
yield if block_given?
+ concerns(options[:concerns]) if options[:concerns]
+
collection do
get :index if parent_resource.actions.include?(:index)
post :create if parent_resource.actions.include?(:create)
@@ -1152,12 +1224,12 @@ module ActionDispatch
get :new
end if parent_resource.actions.include?(:new)
- # TODO: Only accept patch or put depending on config
member do
- get :edit if parent_resource.actions.include?(:edit)
- get :show if parent_resource.actions.include?(:show)
+ get :edit if parent_resource.actions.include?(:edit)
+ get :show if parent_resource.actions.include?(:show)
if parent_resource.actions.include?(:update)
- send default_method_for_update, :update
+ patch :update
+ put :update
end
delete :destroy if parent_resource.actions.include?(:destroy)
end
@@ -1321,7 +1393,7 @@ module ActionDispatch
options[:as] = name_for_action(options[:as], action)
end
- mapping = Mapping.new(@set, @scope, path, options)
+ mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
@@ -1427,18 +1499,18 @@ module ActionDispatch
def nested_options #:nodoc:
options = { :as => parent_resource.member_name }
options[:constraints] = {
- :"#{parent_resource.singular}_id" => id_constraint
- } if id_constraint?
+ parent_resource.nested_param => param_constraint
+ } if param_constraint?
options
end
- def id_constraint? #:nodoc:
- @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
+ def param_constraint? #:nodoc:
+ @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
- def id_constraint #:nodoc:
- @scope[:constraints][:id]
+ def param_constraint #:nodoc:
+ @scope[:constraints][parent_resource.param]
end
def canonical_action?(action, flag) #:nodoc:
@@ -1451,9 +1523,9 @@ module ActionDispatch
def path_for_action(action, path) #:nodoc:
prefix = shallow_scoping? ?
- "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
+ "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
- path = if canonical_action?(action, path.blank?)
+ if canonical_action?(action, path.blank?)
prefix.to_s
else
"#{prefix}/#{action_path(action, path)}"
@@ -1513,15 +1585,122 @@ module ActionDispatch
end
end
+ # Routing Concerns allow you to declare common routes that can be reused
+ # inside others resources and routes.
+ #
+ # concern :commentable do
+ # resources :comments
+ # end
+ #
+ # concern :image_attachable do
+ # resources :images, only: :index
+ # end
+ #
+ # These concerns are used in Resources routing:
+ #
+ # resources :messages, concerns: [:commentable, :image_attachable]
+ #
+ # or in a scope or namespace:
+ #
+ # namespace :posts do
+ # concerns :commentable
+ # end
+ module Concerns
+ # Define a routing concern using a name.
+ #
+ # Concerns may be defined inline, using a block, or handled by
+ # another object, by passing that object as the second parameter.
+ #
+ # The concern object, if supplied, should respond to <tt>call</tt>,
+ # which will receive two parameters:
+ #
+ # * The current mapper
+ # * A hash of options which the concern object may use
+ #
+ # Options may also be used by concerns defined in a block by accepting
+ # a block parameter. So, using a block, you might do something as
+ # simple as limit the actions available on certain resources, passing
+ # standard resource options through the concern:
+ #
+ # concern :commentable do |options|
+ # resources :comments, options
+ # end
+ #
+ # resources :posts, concerns: :commentable
+ # resources :archived_posts do
+ # # Don't allow comments on archived posts
+ # concerns :commentable, only: [:index, :show]
+ # end
+ #
+ # Or, using a callable object, you might implement something more
+ # specific to your application, which would be out of place in your
+ # routes file.
+ #
+ # # purchasable.rb
+ # class Purchasable
+ # def initialize(defaults = {})
+ # @defaults = defaults
+ # end
+ #
+ # def call(mapper, options = {})
+ # options = @defaults.merge(options)
+ # mapper.resources :purchases
+ # mapper.resources :receipts
+ # mapper.resources :returns if options[:returnable]
+ # end
+ # end
+ #
+ # # routes.rb
+ # concern :purchasable, Purchasable.new(returnable: true)
+ #
+ # resources :toys, concerns: :purchasable
+ # resources :electronics, concerns: :purchasable
+ # resources :pets do
+ # concerns :purchasable, returnable: false
+ # end
+ #
+ # Any routing helpers can be used inside a concern. If using a
+ # callable, they're accessible from the Mapper that's passed to
+ # <tt>call</tt>.
+ def concern(name, callable = nil, &block)
+ callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
+ @concerns[name] = callable
+ end
+
+ # Use the named concerns
+ #
+ # resources :posts do
+ # concerns :commentable
+ # end
+ #
+ # concerns also work in any routes helper that you want to use:
+ #
+ # namespace :posts do
+ # concerns :commentable
+ # end
+ def concerns(*args)
+ options = args.extract_options!
+ args.flatten.each do |name|
+ if concern = @concerns[name]
+ concern.call(self, options)
+ else
+ raise ArgumentError, "No concern named #{name} was found!"
+ end
+ end
+ end
+ end
+
def initialize(set) #:nodoc:
@set = set
@scope = { :path_names => @set.resources_path_names }
+ @concerns = {}
end
include Base
include HttpHelpers
include Redirection
include Scoping
+ include Concerns
include Resources
end
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 013cf93dbc..3d7b8878b8 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -1,3 +1,5 @@
+require 'action_controller/model_naming'
+
module ActionDispatch
module Routing
# Polymorphic URL helpers are methods for smart resolution to a named route call when
@@ -43,18 +45,18 @@ module ActionDispatch
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
#
- # == Using with mounted engines
- #
- # If you use mounted engine, there is a possibility that you will need to use
- # polymorphic_url pointing at engine's routes. To do that, just pass proxy used
- # to reach engine's routes as a first argument:
+ # == Usage with mounted engines
#
- # For example:
+ # If you are using a mounted engine and you need to use a polymorphic_url
+ # pointing at the engine's routes, pass in the engine's route proxy as the first
+ # argument to the method. For example:
#
- # polymorphic_url([blog, @post]) # it will call blog.post_path(@post)
- # form_for([blog, @post]) # => "/blog/posts/1
+ # polymorphic_url([blog, @post]) # calls blog.post_path(@post)
+ # form_for([blog, @post]) # => "/blog/posts/1"
#
module PolymorphicRoutes
+ include ActionController::ModelNaming
+
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
#
@@ -97,7 +99,7 @@ module ActionDispatch
end
record = extract_record(record_or_hash_or_array)
- record = record.to_model if record.respond_to?(:to_model)
+ record = convert_to_model(record)
args = Array === record_or_hash_or_array ?
record_or_hash_or_array.dup :
@@ -124,6 +126,8 @@ module ActionDispatch
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end
+ args.collect! { |a| convert_to_model(a) }
+
(proxy || self).send(named_route, *args)
end
@@ -165,7 +169,7 @@ module ActionDispatch
if parent.is_a?(Symbol) || parent.is_a?(String)
parent
else
- ActiveModel::Naming.singular_route_key(parent)
+ model_name_from_record_or_class(parent).singular_route_key
end
end
else
@@ -177,9 +181,9 @@ module ActionDispatch
route << record
elsif record
if inflection == :singular
- route << ActiveModel::Naming.singular_route_key(record)
+ route << model_name_from_record_or_class(record).singular_route_key
else
- route << ActiveModel::Naming.route_key(record)
+ route << model_name_from_record_or_class(record).route_key
end
else
raise ArgumentError, "Nil location provided. Can't build URI."
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 617b24b46a..205ff44b1c 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -1,4 +1,8 @@
require 'action_dispatch/http/request'
+require 'active_support/core_ext/uri'
+require 'active_support/core_ext/array/extract_options'
+require 'rack/utils'
+require 'action_controller/metal/exceptions'
module ActionDispatch
module Routing
@@ -13,6 +17,14 @@ module ActionDispatch
def call(env)
req = Request.new(env)
+ # If any of the path parameters has a 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
+
uri = URI.parse(path(req.symbolized_path_parameters, req))
uri.scheme ||= req.scheme
uri.host ||= req.host
@@ -32,6 +44,25 @@ module ActionDispatch
def path(params, request)
block.call params, request
end
+
+ def inspect
+ "redirect(#{status})"
+ end
+ end
+
+ class PathRedirect < Redirect
+ def path(params, request)
+ (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params))
+ end
+
+ def inspect
+ "redirect(#{status}, #{block})"
+ end
+
+ private
+ def escape(params)
+ Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ end
end
class OptionRedirect < Redirect # :nodoc:
@@ -46,8 +77,21 @@ module ActionDispatch
:params => request.query_parameters
}.merge options
+ if !params.empty? && url_options[:path].match(/%\{\w*\}/)
+ url_options[:path] = (url_options[:path] % escape_path(params))
+ end
+
ActionDispatch::Http::URL.url_for url_options
end
+
+ def inspect
+ "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
+ end
+
+ private
+ def escape_path(params)
+ Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }]
+ end
end
module Redirection
@@ -67,10 +111,13 @@ module ActionDispatch
# params, depending of how many arguments your block accepts. A string is required as a
# return value.
#
- # match 'jokes/:number', :to => redirect do |params, request|
- # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp")
+ # match 'jokes/:number', :to => redirect { |params, request|
+ # path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
# "http://#{request.host_with_port}/#{path}"
- # end
+ # }
+ #
+ # Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
+ # the block to +match+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
#
# The options version of redirect allows you to supply only the parts of the url which need
# to change, it also supports interpolation of the path similar to the first example.
@@ -85,16 +132,12 @@ module ActionDispatch
# match 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
#
def redirect(*args, &block)
- options = args.last.is_a?(Hash) ? args.pop : {}
+ options = args.extract_options!
status = options.delete(:status) || 301
+ path = args.shift
return OptionRedirect.new(status, options) if options.any?
-
- path = args.shift
-
- block = lambda { |params, request|
- (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params)
- } if String === path
+ return PathRedirect.new(status, path) if String === path
block = path if path.respond_to? :call
raise ArgumentError, "redirection argument not supported" unless block
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 6c189fdba6..060d0bfa2f 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,6 +1,5 @@
require 'journey'
require 'forwardable'
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/module/remove_method'
@@ -9,6 +8,12 @@ require 'action_controller/metal/exceptions'
module ActionDispatch
module Routing
class RouteSet #:nodoc:
+ # Since the router holds references to many parts of the system
+ # like engines, controllers and the application itself, inspecting
+ # the route set can actually be really slow, therefore we default
+ # alias inspect to to_s.
+ alias inspect to_s
+
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
class Dispatcher #:nodoc:
@@ -20,6 +25,15 @@ module ActionDispatch
def call(env)
params = env[PARAMETERS_KEY]
+
+ # If any of the path parameters has a invalid encoding then
+ # raise since it's likely to trigger errors further on.
+ params.each do |key, value|
+ unless value.valid_encoding?
+ raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ end
+ end
+
prepare_params!(params)
# Just raise undefined constant errors if a controller was specified as default.
@@ -90,7 +104,25 @@ module ActionDispatch
def initialize
@routes = {}
@helpers = []
- @module = Module.new
+ @module = Module.new do
+ protected
+
+ def handle_positional_args(args, options, segment_keys)
+ inner_options = args.extract_options!
+ result = options.dup
+
+ if args.size > 0
+ keys = segment_keys
+ if args.size < keys.size - 1 # take format into account
+ keys -= self.url_options.keys if self.respond_to?(:url_options)
+ keys -= options.keys
+ end
+ result.merge!(Hash[keys.zip(args)])
+ end
+
+ result.merge!(inner_options)
+ end
+ end
end
def helper_names
@@ -129,43 +161,12 @@ module ActionDispatch
end
private
- def url_helper_name(name, kind = :url)
- :"#{name}_#{kind}"
- end
-
- def hash_access_name(name, kind = :url)
- :"hash_for_#{name}_#{kind}"
- end
def define_named_route_methods(name, route)
- {:url => {:only_path => false}, :path => {:only_path => true}}.each do |kind, opts|
- hash = route.defaults.merge(:use_route => name).merge(opts)
- define_hash_access route, name, kind, hash
- define_url_helper route, name, kind, hash
- end
- end
-
- def define_hash_access(route, name, kind, options)
- selector = hash_access_name(name, kind)
-
- @module.module_eval do
- remove_possible_method selector
-
- define_method(selector) do |*args|
- inner_options = args.extract_options!
- result = options.dup
-
- if args.any?
- result[:_positional_args] = args
- result[:_positional_keys] = route.segment_keys
- end
-
- result.merge(inner_options)
- end
-
- protected selector
- end
- helpers << selector
+ 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)
end
# Create a url helper allowing ordered parameters to be associated
@@ -181,23 +182,51 @@ module ActionDispatch
#
# foo_url(bar, baz, bang, :sort_by => 'baz')
#
- def define_url_helper(route, name, kind, options)
- selector = url_helper_name(name, kind)
- hash_access_method = hash_access_name(name, kind)
-
+ def define_url_helper(route, name, options)
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
- remove_possible_method :#{selector}
- def #{selector}(*args)
- url_for(#{hash_access_method}(*args))
+ remove_possible_method :#{name}
+ def #{name}(*args)
+ if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation?
+ options = #{options.inspect}
+ options.merge!(url_options) if respond_to?(:url_options)
+ options[:path] = "#{optimized_helper(route)}"
+ ActionDispatch::Http::URL.url_for(options)
+ else
+ url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect}))
+ end
end
END_EVAL
- helpers << selector
+
+ helpers << name
+ end
+
+ # Clause check about when we need to generate an optimized helper.
+ def optimize_helper?(route) #:nodoc:
+ route.requirements.except(:controller, :action).empty?
+ end
+
+ # Generates the interpolation to be used in the optimized helper.
+ def optimized_helper(route)
+ string_route = route.ast.to_s
+
+ while string_route.gsub!(/\([^\)]*\)/, "")
+ true
+ end
+
+ route.required_parts.each_with_index do |part, i|
+ # Replace each route parameter
+ # e.g. :id for regular parameter or *path for globbing
+ # with ruby string interpolation code
+ string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}")
+ end
+
+ string_route
end
end
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, :valid_conditions
+ attr_accessor :default_url_options, :request_class
alias :routes :set
@@ -209,17 +238,7 @@ module ActionDispatch
self.named_routes = NamedRouteCollection.new
self.resources_path_names = self.class.default_resources_path_names.dup
self.default_url_options = {}
-
self.request_class = request_class
- @valid_conditions = {}
-
- request_class.public_instance_methods.each { |m|
- @valid_conditions[m.to_sym] = true
- }
- @valid_conditions[:controller] = true
- @valid_conditions[:action] = true
-
- self.valid_conditions.delete(:id)
@append = []
@prepend = []
@@ -251,8 +270,7 @@ module ActionDispatch
def eval_block(block)
if block.arity == 1
raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
- "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/ " <<
- "or add the rails_legacy_mapper gem to your Gemfile"
+ "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
end
mapper = Mapper.new(self)
if default_scope
@@ -299,9 +317,9 @@ module ActionDispatch
end
end
- MountedHelpers.class_eval <<-RUBY
+ MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{name}
- @#{name} ||= _#{name}
+ @_#{name} ||= _#{name}
end
RUBY
end
@@ -318,7 +336,7 @@ module ActionDispatch
# Rails.application.routes.url_helpers.url_for(args)
@_routes = routes
class << self
- delegate :url_for, :to => '@_routes'
+ delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
end
# Make named_routes available in the module singleton
@@ -351,7 +369,7 @@ module ActionDispatch
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
- conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym })
+ conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
route = @set.add_route(app, path, conditions, defaults, name)
named_routes[name] = route if name && !named_routes[name]
@@ -388,21 +406,22 @@ module ActionDispatch
end
private :build_path
- def build_conditions(current_conditions, req_predicates, path_values)
+ def build_conditions(current_conditions, path_values)
conditions = current_conditions.dup
- verbs = conditions[:request_method] || []
-
# Rack-Mount requires that :request_method be a regular expression.
# :request_method represents the HTTP verb that matches this route.
#
# Here we munge values before they get sent on to rack-mount.
+ verbs = conditions[:request_method] || []
unless verbs.empty?
conditions[:request_method] = %r[^#{verbs.join('|')}$]
end
- conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) }
- conditions
+ conditions.keep_if do |k, _|
+ k == :action || k == :controller ||
+ @request_class.public_method_defined?(k) || path_values.include?(k)
+ end
end
private :build_conditions
@@ -419,22 +438,21 @@ module ActionDispatch
attr_reader :options, :recall, :set, :named_route
- def initialize(options, recall, set, extras = false)
+ def initialize(options, recall, set)
@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
+ normalize_controller!
handle_nil_action!
end
def controller
- @controller ||= @options[:controller]
+ @options[:controller]
end
def current_controller
@@ -443,9 +461,7 @@ module ActionDispatch
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
- if named_route_exists?
- @options[key] = @recall.delete(key) if segment_keys.include?(key)
- else
+ if !named_route_exists? || segment_keys.include?(key)
@options[key] = @recall.delete(key)
end
end
@@ -491,10 +507,15 @@ module ActionDispatch
old_parts = current_controller.split('/')
size = controller.count("/") + 1
parts = old_parts[0...-size] << controller
- @controller = @options[:controller] = parts.join("/")
+ @options[:controller] = parts.join("/")
end
end
+ # Remove leading slashes from controllers
+ def normalize_controller!
+ @options[:controller] = controller.sub(%r{^/}, '') if controller
+ end
+
# This handles the case of :action => nil being explicitly passed.
# It is identical to :action => "index"
def handle_nil_action!
@@ -504,20 +525,12 @@ module ActionDispatch
recall[:action] = options.delete(:action) if options[:action] == 'index'
end
+ # Generates a path from routes, returns [path, params]
+ # if no path is returned the formatter will raise Journey::Router::RoutingError
def generate
- path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
-
- raise_routing_error unless path
-
- return [path, params.keys] if @extras
-
- [path, params]
- rescue Journey::Router::RoutingError
- raise_routing_error
- end
-
- def raise_routing_error
- raise ActionController::RoutingError, "No route matches #{options.inspect}"
+ @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
+ rescue Journey::Router::RoutingError => e
+ raise ActionController::UrlGenerationError, "No route matches #{options.inspect} #{e.message}"
end
def different_controller?
@@ -542,40 +555,53 @@ module ActionDispatch
end
def generate_extras(options, recall={})
- generate(options, recall, true)
+ path, params = generate(options, recall)
+ return path, params.keys
end
- def generate(options, recall = {}, extras = false)
- Generator.new(options, recall, self, extras).generate
+ def generate(options, recall = {})
+ Generator.new(options, recall, self).generate
end
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
- :trailing_slash, :anchor, :params, :only_path, :script_name]
+ :trailing_slash, :anchor, :params, :only_path, :script_name,
+ :original_script_name]
+
+ def mounted?
+ false
+ end
+
+ def optimize_routes_generation?
+ !mounted? && default_url_options.empty?
+ end
def _generate_prefix(options = {})
nil
end
+ # The +options+ argument must be +nil+ or a hash whose keys are *symbols*.
def url_for(options)
- options = (options || {}).reverse_merge!(default_url_options)
-
- handle_positional_args(options)
+ options = default_url_options.merge(options || {})
user, password = extract_authentication(options)
- path_segments = options.delete(:_path_segments)
- script_name = options.delete(:script_name)
+ recall = options.delete(:_recall)
- path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
+ original_script_name = options.delete(:original_script_name).presence
+ script_name = options.delete(:script_name).presence || _generate_prefix(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_addition, params = generate(path_options, path_segments || {})
- path << path_addition
+ path, params = generate(path_options, recall || {})
params.merge!(options[:params] || {})
ActionDispatch::Http::URL.url_for(options.merge!({
:path => path,
+ :script_name => script_name,
:params => params,
:user => user,
:password => password
@@ -589,6 +615,7 @@ module ActionDispatch
def recognize_path(path, environment = {})
method = (environment[:method] || "GET").to_s.upcase
path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
+ extras = environment[:extras] || {}
begin
env = Rack::MockRequest.env_for(path, {:method => method})
@@ -598,6 +625,7 @@ module ActionDispatch
req = @request_class.new(env)
@router.recognize(req) do |route, matches, params|
+ params.merge!(extras)
params.each do |key, value|
if value.is_a?(String)
value = value.dup.force_encoding(Encoding::BINARY)
@@ -611,9 +639,13 @@ module ActionDispatch
dispatcher = dispatcher.app
end
- if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
- dispatcher.prepare_params!(params)
- return params
+ if dispatcher.is_a?(Dispatcher)
+ if dispatcher.controller(params, false)
+ dispatcher.prepare_params!(params)
+ return params
+ else
+ raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
+ end
end
end
@@ -630,16 +662,6 @@ module ActionDispatch
end
end
- 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
-
- # Tell url_for to skip default_url_options
- options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
- 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 ee6616c5d3..d4cd537048 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -68,7 +68,7 @@ module ActionDispatch
# 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:
+ # you can do that by including Rails.application.routes.url_helpers in your class:
#
# class User < ActiveRecord::Base
# include Rails.application.routes.url_helpers
@@ -95,6 +95,8 @@ module ActionDispatch
self.default_url_options = {}
end
+
+ include(*_url_for_modules) if respond_to?(:_url_for_modules)
end
def initialize(*)
@@ -102,6 +104,9 @@ module ActionDispatch
super
end
+ # Hook overridden in controller to add request information
+ # with `default_url_options`. Application logic should not
+ # go into url_options.
def url_options
default_url_options
end
@@ -129,8 +134,6 @@ module ActionDispatch
# 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
@@ -141,10 +144,12 @@ module ActionDispatch
# # => 'http://somehost.org/tasks/testing?number=33'
def url_for(options = nil)
case options
+ when nil
+ _routes.url_for(url_options.symbolize_keys)
+ when Hash
+ _routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
when String
options
- when nil, Hash
- _routes.url_for((options || {}).symbolize_keys.reverse_merge!(url_options))
else
polymorphic_url(options)
end
@@ -152,6 +157,11 @@ module ActionDispatch
protected
+ def optimize_routes_generation?
+ return @_optimized_routes if defined?(@_optimized_routes)
+ @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty?
+ end
+
def _with_routes(routes)
old_routes, @_routes = @_routes, routes
yield
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
index edea6dab39..6c61d4e61a 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
@@ -1,15 +1,12 @@
-require 'action_controller/vendor/html-scanner'
+require 'action_view/vendor/html-scanner'
module ActionDispatch
module Assertions
module DomAssertions
# \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
#
- # ==== Examples
- #
# # assert that the referenced method generates the appropriate HTML string
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
- #
def assert_dom_equal(expected, actual, message = "")
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
@@ -18,11 +15,8 @@ module ActionDispatch
# The negated form of +assert_dom_equivalent+.
#
- # ==== Examples
- #
# # assert that the referenced method does not generate the specified HTML string
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
- #
def assert_dom_not_equal(expected, actual, message = "")
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 094cfbfc76..b15e0446de 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -1,14 +1,11 @@
-require 'active_support/core_ext/object/inclusion'
module ActionDispatch
module Assertions
# A small suite of assertions that test responses from \Rails applications.
module ResponseAssertions
- extend ActiveSupport::Concern
-
# Asserts that the response is one of the following types:
#
- # * <tt>:success</tt> - Status code was 200
+ # * <tt>:success</tt> - Status code was in the 200-299 range
# * <tt>:redirect</tt> - Status code was in the 300-399 range
# * <tt>:missing</tt> - Status code was 404
# * <tt>:error</tt> - Status code was in the 500-599 range
@@ -17,14 +14,11 @@ module ActionDispatch
# or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
#
- # ==== Examples
- #
# # assert that the response was a redirection
# assert_response :redirect
#
# # assert that the response code was status code 401 (unauthorized)
# assert_response 401
- #
def assert_response(type, message = nil)
message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>"
@@ -33,7 +27,7 @@ module ActionDispatch
assert @response.send("#{type}?"), message
else
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
- assert_equal @response.response_code, code, message
+ assert_equal code, @response.response_code, message
end
else
assert_equal type, @response.response_code, message
@@ -44,8 +38,6 @@ module ActionDispatch
# This match can be partial, such that <tt>assert_redirected_to(:controller => "weblog")</tt> will also
# match the redirection of <tt>redirect_to(:controller => "weblog", :action => "show")</tt> and so on.
#
- # ==== Examples
- #
# # assert that the redirection was to the "index" action on the WeblogController
# assert_redirected_to :controller => "weblog", :action => "index"
#
@@ -55,15 +47,17 @@ module ActionDispatch
# # assert that the redirection was to the url for @customer
# assert_redirected_to @customer
#
+ # # asserts that the redirection matches the regular expression
+ # assert_redirected_to %r(\Ahttp://example.org)
def assert_redirected_to(options = {}, message=nil)
assert_response(:redirect, message)
- return true if options == @response.location
+ return true if options === @response.location
redirect_is = normalize_argument_to_redirection(@response.location)
redirect_expected = normalize_argument_to_redirection(options)
message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
- assert_equal redirect_expected, redirect_is, message
+ assert_operator redirect_expected, :===, redirect_is, message
end
private
@@ -73,17 +67,21 @@ module ActionDispatch
end
def normalize_argument_to_redirection(fragment)
- case fragment
- when %r{^\w[A-Za-z\d+.-]*:.*}
- fragment
- when String
- @request.protocol + @request.host_with_port + fragment
- when :back
- raise RedirectBackError unless refer = @request.headers["Referer"]
- refer
- else
- @controller.url_for(fragment)
- end.gsub(/[\r\n]/, '')
+ normalized = case fragment
+ when Regexp
+ fragment
+ when %r{^\w[A-Za-z\d+.-]*:.*}
+ fragment
+ when String
+ @request.protocol + @request.host_with_port + fragment
+ when :back
+ raise RedirectBackError unless refer = @request.headers["Referer"]
+ refer
+ else
+ @controller.url_for(fragment)
+ end
+
+ normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 1552676fbb..9de545b3c5 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -26,7 +26,6 @@ module ActionDispatch
#
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
#
- # ==== Examples
# # Check the default route (i.e., the index action)
# assert_recognizes({:controller => 'items', :action => 'index'}, 'items')
#
@@ -39,10 +38,9 @@ module ActionDispatch
# # Test a custom route
# assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1')
def assert_recognizes(expected_options, path, extras={}, message=nil)
- request = recognized_request_for(path)
+ request = recognized_request_for(path, extras)
expected_options = expected_options.clone
- extras.each_key { |key| expected_options.delete key } unless extras.nil?
expected_options.stringify_keys!
@@ -58,7 +56,6 @@ module ActionDispatch
#
# The +defaults+ parameter is unused.
#
- # ==== Examples
# # Asserts that the default action is generated for a route with no action
# assert_generates "/items", :controller => "items", :action => "index"
#
@@ -72,11 +69,9 @@ module ActionDispatch
# assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" }
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
if expected_path =~ %r{://}
- begin
+ fail_on(URI::InvalidURIError) do
uri = URI.parse(expected_path)
expected_path = uri.path.to_s.empty? ? "/" : uri.path
- rescue URI::InvalidURIError => e
- raise ActionController::RoutingError, e.message
end
else
expected_path = "/#{expected_path}" unless expected_path.first == '/'
@@ -101,7 +96,6 @@ module ActionDispatch
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
# +message+ parameter allows you to specify a custom error message to display upon failure.
#
- # ==== Examples
# # Assert a basic route: a controller with the default action (index)
# assert_routing '/home', :controller => 'home', :action => 'index'
#
@@ -133,16 +127,13 @@ module ActionDispatch
# with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
- # will create some routes using <tt>map.draw { map.connect ... }</tt>:
+ # will create some routes using <tt>set.draw { match ... }</tt>:
#
# with_routing do |set|
- # set.draw do |map|
- # map.connect ':controller/:action/:id'
- # assert_equal(
- # ['/content/10/show', {}],
- # map.generate(:controller => 'content', :id => 10, :action => 'show')
- # end
+ # set.draw do
+ # resources :users
# end
+ # assert_equal "/users", users_path
# end
#
def with_routing
@@ -181,7 +172,7 @@ module ActionDispatch
private
# Recognizes the route for a given path.
- def recognized_request_for(path)
+ def recognized_request_for(path, extras = {})
if path.is_a?(Hash)
method = path[:method]
path = path[:path]
@@ -193,14 +184,12 @@ module ActionDispatch
request = ActionController::TestRequest.new
if path =~ %r{://}
- begin
+ fail_on(URI::InvalidURIError) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
request.host = uri.host if uri.host
request.port = uri.port if uri.port
request.path = uri.path.to_s.empty? ? "/" : uri.path
- rescue URI::InvalidURIError => e
- raise ActionController::RoutingError, e.message
end
else
path = "/#{path}" unless path.first == "/"
@@ -209,11 +198,21 @@ module ActionDispatch
request.request_method = method if method
- params = @routes.recognize_path(path, { :method => method })
+ params = fail_on(ActionController::RoutingError) do
+ @routes.recognize_path(path, { :method => method, :extras => extras })
+ end
request.path_parameters = params.with_indifferent_access
request
end
+
+ def fail_on(exception_class)
+ begin
+ yield
+ rescue exception_class => e
+ raise MiniTest::Assertion, e.message
+ end
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
index 8eed85bce2..9388d44eef 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
@@ -1,4 +1,4 @@
-require 'action_controller/vendor/html-scanner'
+require 'action_view/vendor/html-scanner'
require 'active_support/core_ext/object/inclusion'
#--
@@ -39,7 +39,6 @@ module ActionDispatch
# The selector may be a CSS selector expression (String), an expression
# with substitution values (Array) or an HTML::Selector object.
#
- # ==== Examples
# # Selects all div tags
# divs = css_select("div")
#
@@ -58,7 +57,6 @@ module ActionDispatch
# inputs = css_select(form, "input")
# ...
# end
- #
def css_select(*args)
# See assert_select to understand what's going on here.
arg = args.shift
@@ -269,6 +267,7 @@ module ActionDispatch
end
end
text.strip! unless NO_STRIP.include?(match.name)
+ text.sub!(/\A\n/, '') if match.name == "textarea"
unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text)
true
@@ -339,7 +338,6 @@ module ActionDispatch
# The content of each element is un-encoded, and wrapped in the root
# element +encoded+. It then calls the block with all un-encoded elements.
#
- # ==== Examples
# # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
# assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
# # Select each entry item and then the title item
@@ -400,8 +398,6 @@ module ActionDispatch
# You must enable deliveries for this assertion to work, use:
# ActionMailer::Base.perform_deliveries = true
#
- # ==== Examples
- #
# assert_select_email do
# assert_select "h1", "Email alert"
# end
@@ -412,7 +408,6 @@ module ActionDispatch
# # Work with items here...
# end
# end
- #
def assert_select_email(&block)
deliveries = ActionMailer::Base.deliveries
assert !deliveries.empty?, "No e-mail in delivery list"
diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
index 5c735e61b2..2e38266aba 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
@@ -1,4 +1,4 @@
-require 'action_controller/vendor/html-scanner'
+require 'action_view/vendor/html-scanner'
module ActionDispatch
module Assertions
@@ -48,8 +48,6 @@ module ActionDispatch
# * if the condition is +true+, the value must not be +nil+.
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
- # === Examples
- #
# # Assert that there is a "span" tag
# assert_tag :tag => "span"
#
@@ -104,7 +102,6 @@ module ActionDispatch
# Identical to +assert_tag+, but asserts that a matching tag does _not_
# exist. (See +assert_tag+ for a full discussion of the syntax.)
#
- # === Examples
# # Assert that there is not a "div" containing a "p"
# assert_no_tag :tag => "div", :descendant => { :tag => "p" }
#
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 62b3a344f8..a8b27ffafd 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -1,7 +1,6 @@
require 'stringio'
require 'uri'
require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/object/inclusion'
require 'active_support/core_ext/object/try'
require 'rack/test'
@@ -17,10 +16,10 @@ module ActionDispatch
# a Hash, or a String that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or
# <tt>multipart/form-data</tt>).
- # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
- # automatically be upcased, with the prefix 'HTTP_' added if needed.
+ # - +headers+: Additional headers to pass, as a Hash. The headers will be
+ # merged into the Rack env hash.
#
- # This method returns an Response object, which one can use to
+ # This method returns a Response object, which one can use to
# inspect the details of the response. Furthermore, if this method was
# called from an ActionDispatch::IntegrationTest object, then that
# object's <tt>@response</tt> instance variable will point to the same
@@ -73,8 +72,7 @@ module ActionDispatch
#
# The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or
# +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart
- # string; the headers are a hash. Keys are automatically upcased and
- # prefixed with 'HTTP_' if not already.
+ # string; the headers are a hash.
def xml_http_request(request_method, path, parameters = nil, headers = nil)
headers ||= {}
headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@@ -194,16 +192,26 @@ module ActionDispatch
# 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 }
+ 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)
+ end
end
reset!
end
- remove_method :default_url_options
- def default_url_options
- { :host => host, :protocol => https? ? "https" : "http" }
+ def url_options
+ @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)
+ url_options.reverse_merge!(@app.routes.default_url_options)
+ end
+
+ url_options.reverse_merge!(:host => host, :protocol => https? ? "https" : "http")
+ end
end
# Resets the instance. This can be used to reset the state information
@@ -216,6 +224,7 @@ module ActionDispatch
@controller = @request = @response = nil
@_mock_session = nil
@request_count = 0
+ @url_options = nil
self.host = DEFAULT_HOST
self.remote_addr = "127.0.0.1"
@@ -310,6 +319,7 @@ module ActionDispatch
response = _mock_session.last_response
@response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body)
@html_document = nil
+ @url_options = nil
@controller = session.last_request.env['action_controller.instance']
@@ -330,12 +340,12 @@ module ActionDispatch
@integration_session = Integration::Session.new(app)
end
- %w(get post put patch head delete options cookies assigns
+ %w(get post patch put head delete options cookies assigns
xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args|
reset! unless integration_session
# reset the html_document variable, but only for new get/post calls
- @html_document = nil unless method.in?(["cookies", "assigns"])
+ @html_document = nil unless method == 'cookies' || method == 'assigns'
integration_session.__send__(method, *args).tap do
copy_session_variables!
end
@@ -367,12 +377,14 @@ module ActionDispatch
end
end
- extend ActiveSupport::Concern
- include ActionDispatch::Routing::UrlFor
+ def default_url_options
+ reset! unless integration_session
+ integration_session.default_url_options
+ end
- def url_options
+ def default_url_options=(options)
reset! unless integration_session
- integration_session.url_options
+ integration_session.default_url_options = options
end
def respond_to?(method, include_private = false)
@@ -476,6 +488,7 @@ module ActionDispatch
class IntegrationTest < ActiveSupport::TestCase
include Integration::Runner
include ActionController::TemplateAssertions
+ include ActionDispatch::Routing::UrlFor
@@app = nil
@@ -495,5 +508,10 @@ module ActionDispatch
def app
super || self.class.app
end
+
+ def url_options
+ reset! unless integration_session
+ integration_session.url_options
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index 7280e9a93b..c63778f870 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -1,6 +1,4 @@
-require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/hash/reverse_merge'
require 'rack/utils'
module ActionDispatch
@@ -12,8 +10,8 @@ module ActionDispatch
end
def initialize(env = {})
- env = Rails.application.env_config.merge(env) if defined?(Rails.application)
- super(DEFAULT_ENV.merge(env))
+ env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
+ super(default_env.merge(env))
self.host = 'test.host'
self.remote_addr = '0.0.0.0'
@@ -70,5 +68,11 @@ module ActionDispatch
def cookies
@cookies ||= {}.with_indifferent_access
end
+
+ private
+
+ def default_env
+ DEFAULT_ENV
+ end
end
end