diff options
Diffstat (limited to 'actionpack/lib/action_dispatch')
29 files changed, 624 insertions, 240 deletions
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 0b895e7860..a7f93b780e 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 @@ -86,21 +85,29 @@ module ActionDispatch CACHE_CONTROL = "Cache-Control".freeze SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate] + def cache_control_segments + if cache_control = self[CACHE_CONTROL] + cache_control.delete(' ').split(',') + else + [] + end + end + def cache_control_headers cache_control = {} - if cc = self[CACHE_CONTROL] - cc.delete(' ').split(',').each do |segment| - directive, argument = segment.split('=', 2) - case directive - when *SPESHUL_KEYS - key = directive.tr('-', '_') - cache_control[key.to_sym] = argument || true - else - cache_control[:extras] ||= [] - cache_control[:extras] << segment - end + + 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 diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 6413929be3..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' diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index e31f3b823d..0f98e84788 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -80,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 fe39c220a5..3c1f1458ea 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,11 +1,10 @@ require 'set' require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/object/blank' 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,14 +23,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) - end + class << self + def [](type) + return type if type.is_a?(Type) + Type.lookup_by_extension(type) + end - def self.fetch(type) - return type if type.is_a?(Type) - EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } + 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: @@ -62,32 +63,84 @@ module Mime # 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)\/\*/ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/ @@ -126,75 +179,30 @@ module Mime def parse(accept_header) if accept_header !~ /,/ accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first - if accept_header =~ TRAILING_STAR_REGEXP - parse_data_with_trailing_star($1) - else - [Mime::Type.lookup(accept_header)] - end + 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| + 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 - - # delete text_xml from the list - list.delete_at(text_xml) + params = parse_trailing_star(params) || [params] - 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 + 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>. # @@ -274,18 +282,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 bcfd0b0d00..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 diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 8cea17c7a6..b8ebeb408f 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -38,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 @@ -119,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: @@ -216,8 +227,11 @@ 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 diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index cc46f9983c..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 @@ -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: @@ -59,15 +58,55 @@ module ActionDispatch # :nodoc: 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=/) @@ -80,6 +119,23 @@ 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 @@ -105,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 @@ -126,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) @@ -151,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 @@ -189,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 @@ -200,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/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 4266ec042e..8aa02ec482 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -87,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 771f075275..ba5d332d49 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,5 +1,5 @@ -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 < Rack::Request 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/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/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 6fff94707c..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 diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 64159fa8e7..7c12590c49 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -3,7 +3,6 @@ require 'rack/request' require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' require 'action_dispatch/request/session' -require 'active_support/core_ext/object/blank' 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 7efc094f98..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' @@ -45,6 +44,14 @@ module ActionDispatch 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 def unpacked_cookie_data(env) 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 177d383e94..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,8 +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. + Routes match in priority from top to bottom </p> -<%= render :template => "rescues/_trace" %> +<p><pre><%= @routes %></pre></p> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 62f906219c..ccc0435a39 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -19,10 +19,19 @@ module ActionDispatch :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::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 index d8bcc28613..a05a23d953 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -2,7 +2,7 @@ require 'rack/session/abstract/id' module ActionDispatch class Request < Rack::Request - # SessionHash is responsible to lazily load the session from store. + # 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: @@ -70,9 +70,12 @@ module ActionDispatch def destroy clear options = self.options || {} - @by.send(:destroy_session, @env, options[:id], options) - options[:id] = nil + 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) 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 53a4afecb3..ddb34a2394 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,7 +1,6 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/enumerable' require 'active_support/inflector' require 'action_dispatch/routing/redirection' @@ -262,7 +261,7 @@ 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 @@ -404,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 @@ -430,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 @@ -437,9 +444,10 @@ 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, :via => :all)) + match(path, options.merge(:to => app, :anchor => false, :format => false)) define_generate_prefix(app, options[:as]) self @@ -749,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 # @@ -902,7 +910,7 @@ 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, :param] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: @@ -913,7 +921,7 @@ module ActionDispatch @path = (options[:path] || @name).to_s @controller = (options[:controller] || @name).to_s @as = options[:as] - @param = options[:param] || :id + @param = (options[:param] || :id).to_sym @options = options end @@ -961,12 +969,18 @@ module ActionDispatch "#{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}_#{param}" + "#{path}/:#{nested_param}" end end @@ -1033,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) @@ -1176,6 +1192,10 @@ module ActionDispatch # 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> @@ -1193,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) @@ -1477,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: @@ -1501,7 +1523,7 @@ 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] if canonical_action?(action, path.blank?) prefix.to_s @@ -1563,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/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 1d6ca0c78d..1fe40d8458 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' @@ -162,19 +161,12 @@ module ActionDispatch end private - def url_helper_name(name, only_path) - if only_path - :"#{name}_path" - else - :"#{name}_url" - end - end def define_named_route_methods(name, route) - [true, false].each do |only_path| - hash = route.defaults.merge(:use_route => name, :only_path => only_path) - define_url_helper route, name, hash - end + 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 @@ -191,11 +183,9 @@ module ActionDispatch # foo_url(bar, baz, bang, :sort_by => 'baz') # def define_url_helper(route, name, options) - selector = url_helper_name(name, options[:only_path]) - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*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) @@ -207,7 +197,7 @@ module ActionDispatch end END_EVAL - helpers << selector + helpers << name end # Clause check about when we need to generate an optimized helper. @@ -236,7 +226,7 @@ module ActionDispatch 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 @@ -248,13 +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 = { :controller => true, :action => true } - request_class.public_instance_methods.each { |m| - @valid_conditions[m] = true - } - @valid_conditions.delete(:id) @append = [] @prepend = [] @@ -333,7 +317,7 @@ module ActionDispatch end end - MountedHelpers.class_eval <<-RUBY + MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def #{name} @#{name} ||= _#{name} end @@ -385,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] @@ -422,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 @@ -453,12 +438,11 @@ 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! @@ -477,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 @@ -543,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? @@ -581,15 +555,17 @@ 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 @@ -608,13 +584,19 @@ module ActionDispatch options = default_url_options.merge(options || {}) user, password = extract_authentication(options) - path_segments = options.delete(:_path_segments) - script_name = options.delete(:script_name).presence || _generate_prefix(options) + recall = options.delete(:_recall) + + 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, params = generate(path_options, path_segments || {}) + path, params = generate(path_options, recall || {}) params.merge!(options[:params] || {}) ActionDispatch::Http::URL.url_for(options.merge!({ diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index fd3bed7e8f..d4cd537048 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -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,7 +104,7 @@ module ActionDispatch super end - # Hook overriden in controller to add request information + # Hook overridden in controller to add request information # with `default_url_options`. Application logic should not # go into url_options. def url_options diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 7dc3d0f97c..6c61d4e61a 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -1,4 +1,4 @@ -require 'action_controller/vendor/html-scanner' +require 'action_view/vendor/html-scanner' module ActionDispatch module Assertions diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index b4c8f839ac..b15e0446de 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' module ActionDispatch module Assertions diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 41fa3a4b95..9de545b3c5 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -127,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 diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 5f9c3bbf48..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' #-- diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb index 68f1347e7c..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 diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index c5ef46ad30..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' @@ -346,7 +345,7 @@ module ActionDispatch 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 diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 639ae6f398..c63778f870 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/indifferent_access' require 'rack/utils' |