From 319ae4628f4e0058de3e40e4ca7791b17e45e70c Mon Sep 17 00:00:00 2001 From: Joshua Peek Date: Tue, 27 Jan 2009 18:54:01 -0600 Subject: Move HTTP libs and middleware into ActionDispatch component --- actionpack/lib/action_controller.rb | 32 +- actionpack/lib/action_controller/base/base.rb | 6 +- actionpack/lib/action_controller/base/headers.rb | 33 -- .../lib/action_controller/dispatch/dispatcher.rb | 4 +- .../lib/action_controller/dispatch/middlewares.rb | 21 + .../action_controller/dispatch/params_parser.rb | 71 --- .../action_controller/dispatch/rack/failsafe.rb | 52 --- .../lib/action_controller/dispatch/rack/lock.rb | 16 - .../dispatch/rack/middleware_stack.rb | 109 ----- .../action_controller/dispatch/rack/middlewares.rb | 21 - .../lib/action_controller/dispatch/request.rb | 492 --------------------- .../action_controller/dispatch/request_parser.rb | 315 ------------- .../lib/action_controller/dispatch/rescue.rb | 4 +- .../lib/action_controller/dispatch/response.rb | 255 ----------- .../action_controller/dispatch/rewindable_input.rb | 28 -- .../lib/action_controller/dispatch/status_codes.rb | 88 ---- .../action_controller/dispatch/uploaded_file.rb | 44 -- .../dispatch/url_encoded_pair_parser.rb | 155 ------- .../lib/action_controller/mime/default_types.rb | 21 - actionpack/lib/action_controller/mime/type.rb | 214 --------- actionpack/lib/action_controller/rack_ext.rb | 3 - actionpack/lib/action_controller/rack_ext/lock.rb | 21 - .../lib/action_controller/rack_ext/multipart.rb | 22 - .../lib/action_controller/rack_ext/parse_query.rb | 18 - .../lib/action_controller/routing/route_set.rb | 2 +- .../action_controller/session/abstract_store.rb | 166 ------- .../lib/action_controller/session/cookie_store.rb | 222 ---------- .../lib/action_controller/session/management.rb | 2 +- .../action_controller/session/mem_cache_store.rb | 51 --- .../testing/assertions/response.rb | 4 +- .../lib/action_controller/testing/integration.rb | 6 +- .../lib/action_controller/testing/process.rb | 4 +- actionpack/lib/action_dispatch.rb | 64 +++ actionpack/lib/action_dispatch/http/headers.rb | 33 ++ actionpack/lib/action_dispatch/http/mime_type.rb | 214 +++++++++ actionpack/lib/action_dispatch/http/mime_types.rb | 21 + actionpack/lib/action_dispatch/http/request.rb | 492 +++++++++++++++++++++ actionpack/lib/action_dispatch/http/response.rb | 255 +++++++++++ .../lib/action_dispatch/http/status_codes.rb | 88 ++++ .../lib/action_dispatch/middleware/failsafe.rb | 52 +++ .../action_dispatch/middleware/params_parser.rb | 71 +++ .../action_dispatch/middleware/rewindable_input.rb | 28 ++ .../middleware/session/abstract_store.rb | 166 +++++++ .../middleware/session/cookie_store.rb | 222 ++++++++++ .../middleware/session/mem_cache_store.rb | 51 +++ actionpack/lib/action_dispatch/rack.rb | 3 + actionpack/lib/action_dispatch/rack/lock.rb | 21 + actionpack/lib/action_dispatch/rack/multipart.rb | 22 + actionpack/lib/action_dispatch/rack/parse_query.rb | 18 + .../lib/action_dispatch/utils/middleware_stack.rb | 109 +++++ .../lib/action_dispatch/utils/uploaded_file.rb | 44 ++ .../utils/url_encoded_pair_parser.rb | 155 +++++++ 52 files changed, 2169 insertions(+), 2462 deletions(-) delete mode 100644 actionpack/lib/action_controller/base/headers.rb create mode 100644 actionpack/lib/action_controller/dispatch/middlewares.rb delete mode 100644 actionpack/lib/action_controller/dispatch/params_parser.rb delete mode 100644 actionpack/lib/action_controller/dispatch/rack/failsafe.rb delete mode 100644 actionpack/lib/action_controller/dispatch/rack/lock.rb delete mode 100644 actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb delete mode 100644 actionpack/lib/action_controller/dispatch/rack/middlewares.rb delete mode 100755 actionpack/lib/action_controller/dispatch/request.rb delete mode 100644 actionpack/lib/action_controller/dispatch/request_parser.rb delete mode 100644 actionpack/lib/action_controller/dispatch/response.rb delete mode 100644 actionpack/lib/action_controller/dispatch/rewindable_input.rb delete mode 100644 actionpack/lib/action_controller/dispatch/status_codes.rb delete mode 100644 actionpack/lib/action_controller/dispatch/uploaded_file.rb delete mode 100644 actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb delete mode 100644 actionpack/lib/action_controller/mime/default_types.rb delete mode 100644 actionpack/lib/action_controller/mime/type.rb delete mode 100644 actionpack/lib/action_controller/rack_ext.rb delete mode 100644 actionpack/lib/action_controller/rack_ext/lock.rb delete mode 100644 actionpack/lib/action_controller/rack_ext/multipart.rb delete mode 100644 actionpack/lib/action_controller/rack_ext/parse_query.rb delete mode 100644 actionpack/lib/action_controller/session/abstract_store.rb delete mode 100644 actionpack/lib/action_controller/session/cookie_store.rb delete mode 100644 actionpack/lib/action_controller/session/mem_cache_store.rb create mode 100644 actionpack/lib/action_dispatch.rb create mode 100644 actionpack/lib/action_dispatch/http/headers.rb create mode 100644 actionpack/lib/action_dispatch/http/mime_type.rb create mode 100644 actionpack/lib/action_dispatch/http/mime_types.rb create mode 100755 actionpack/lib/action_dispatch/http/request.rb create mode 100644 actionpack/lib/action_dispatch/http/response.rb create mode 100644 actionpack/lib/action_dispatch/http/status_codes.rb create mode 100644 actionpack/lib/action_dispatch/middleware/failsafe.rb create mode 100644 actionpack/lib/action_dispatch/middleware/params_parser.rb create mode 100644 actionpack/lib/action_dispatch/middleware/rewindable_input.rb create mode 100644 actionpack/lib/action_dispatch/middleware/session/abstract_store.rb create mode 100644 actionpack/lib/action_dispatch/middleware/session/cookie_store.rb create mode 100644 actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb create mode 100644 actionpack/lib/action_dispatch/rack.rb create mode 100644 actionpack/lib/action_dispatch/rack/lock.rb create mode 100644 actionpack/lib/action_dispatch/rack/multipart.rb create mode 100644 actionpack/lib/action_dispatch/rack/parse_query.rb create mode 100644 actionpack/lib/action_dispatch/utils/middleware_stack.rb create mode 100644 actionpack/lib/action_dispatch/utils/uploaded_file.rb create mode 100644 actionpack/lib/action_dispatch/utils/url_encoded_pair_parser.rb (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index d973cbb382..eb596ba40e 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -31,17 +31,14 @@ rescue LoadError end end -gem 'rack', '>= 0.9.0' -require 'rack' -require 'action_controller/rack_ext' - require File.join(File.dirname(__FILE__), "action_pack") module ActionController # TODO: Review explicit to see if they will automatically be handled by # the initilizer if they are really needed. def self.load_all! - [Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter] + [Base, Request, Response, UrlRewriter, UrlWriter] + [ActionDispatch::Http::Headers] end autoload :Base, 'action_controller/base/base' @@ -49,7 +46,6 @@ module ActionController autoload :Caching, 'action_controller/caching' autoload :Cookies, 'action_controller/base/cookies' autoload :Dispatcher, 'action_controller/dispatch/dispatcher' - autoload :Failsafe, 'action_controller/dispatch/rack/failsafe' autoload :Filters, 'action_controller/base/chained/filters' autoload :Flash, 'action_controller/base/chained/flash' autoload :Helpers, 'action_controller/base/helpers' @@ -57,32 +53,21 @@ module ActionController autoload :Integration, 'action_controller/testing/integration' autoload :IntegrationTest, 'action_controller/testing/integration' autoload :Layout, 'action_controller/base/layout' - autoload :Lock, 'action_controller/dispatch/rack/lock' - autoload :MiddlewareStack, 'action_controller/dispatch/rack/middleware_stack' autoload :MimeResponds, 'action_controller/mime/responds' - autoload :ParamsParser, 'action_controller/dispatch/params_parser' autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes' autoload :RecordIdentifier, 'action_controller/record_identifier' autoload :Redirector, 'action_controller/base/redirect' autoload :Renderer, 'action_controller/base/render' - autoload :Request, 'action_controller/dispatch/request' autoload :RequestForgeryProtection, 'action_controller/base/request_forgery_protection' - autoload :RequestParser, 'action_controller/dispatch/request_parser' autoload :Rescue, 'action_controller/dispatch/rescue' autoload :Resources, 'action_controller/routing/resources' autoload :Responder, 'action_controller/base/responder' - autoload :Response, 'action_controller/dispatch/response' - autoload :RewindableInput, 'action_controller/dispatch/rewindable_input' autoload :Routing, 'action_controller/routing' autoload :SessionManagement, 'action_controller/session/management' - autoload :StatusCodes, 'action_controller/dispatch/status_codes' autoload :Streaming, 'action_controller/base/streaming' autoload :TestCase, 'action_controller/testing/test_case' autoload :TestProcess, 'action_controller/testing/process' autoload :Translation, 'action_controller/translation' - autoload :UploadedFile, 'action_controller/dispatch/uploaded_file' - autoload :UploadedStringIO, 'action_controller/dispatch/uploaded_file' - autoload :UploadedTempfile, 'action_controller/dispatch/uploaded_file' autoload :UrlEncodedPairParser, 'action_controller/dispatch/url_encoded_pair_parser' autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter' autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter' @@ -96,20 +81,9 @@ module ActionController autoload :SelectorAssertions, 'action_controller/testing/assertions/selector' autoload :TagAssertions, 'action_controller/testing/assertions/tag' end - - module Http - autoload :Headers, 'action_controller/base/headers' - end - - module Session - autoload :AbstractStore, 'action_controller/session/abstract_store' - autoload :CookieStore, 'action_controller/session/cookie_store' - autoload :MemCacheStore, 'action_controller/session/mem_cache_store' - end end -autoload :Mime, 'action_controller/mime/type' - autoload :HTML, 'action_controller/vendor/html-scanner' +require 'action_dispatch' require 'action_view' diff --git a/actionpack/lib/action_controller/base/base.rb b/actionpack/lib/action_controller/base/base.rb index 84371643d7..c9c23008cd 100644 --- a/actionpack/lib/action_controller/base/base.rb +++ b/actionpack/lib/action_controller/base/base.rb @@ -232,7 +232,7 @@ module ActionController #:nodoc: # class Base - include StatusCodes + include ActionDispatch::StatusCodes cattr_reader :protected_instance_variables # Controller specific instance variables which will not be accessible inside views. @@ -367,8 +367,8 @@ module ActionController #:nodoc: class << self def call(env) # HACK: For global rescue to have access to the original request and response - request = env["action_controller.rescue.request"] ||= Request.new(env) - response = env["action_controller.rescue.response"] ||= Response.new + request = env["action_controller.rescue.request"] ||= ActionDispatch::Request.new(env) + response = env["action_controller.rescue.response"] ||= ActionDispatch::Response.new process(request, response) end diff --git a/actionpack/lib/action_controller/base/headers.rb b/actionpack/lib/action_controller/base/headers.rb deleted file mode 100644 index 139669c66f..0000000000 --- a/actionpack/lib/action_controller/base/headers.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'active_support/memoizable' - -module ActionController - module Http - class Headers < ::Hash - extend ActiveSupport::Memoizable - - def initialize(*args) - if args.size == 1 && args[0].is_a?(Hash) - super() - update(args[0]) - else - super - end - end - - def [](header_name) - if include?(header_name) - super - else - super(env_name(header_name)) - end - end - - private - # Converts a HTTP header name to an environment variable name. - def env_name(header_name) - "HTTP_#{header_name.upcase.gsub(/-/, '_')}" - end - memoize :env_name - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/dispatcher.rb b/actionpack/lib/action_controller/dispatch/dispatcher.rb index 714e270781..e205245f13 100644 --- a/actionpack/lib/action_controller/dispatch/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatch/dispatcher.rb @@ -45,8 +45,8 @@ module ActionController end cattr_accessor :middleware - self.middleware = MiddlewareStack.new do |middleware| - middlewares = File.join(File.dirname(__FILE__), "rack", "middlewares.rb") + self.middleware = ActionDispatch::MiddlewareStack.new do |middleware| + middlewares = File.join(File.dirname(__FILE__), "middlewares.rb") middleware.instance_eval(File.read(middlewares)) end diff --git a/actionpack/lib/action_controller/dispatch/middlewares.rb b/actionpack/lib/action_controller/dispatch/middlewares.rb new file mode 100644 index 0000000000..3bf3dbebab --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/middlewares.rb @@ -0,0 +1,21 @@ +use "Rack::Lock", :if => lambda { + !ActionController::Base.allow_concurrency +} + +use "ActionDispatch::Failsafe" + +["ActionDispatch::Session::CookieStore", + "ActionDispatch::Session::MemCacheStore", + "ActiveRecord::SessionStore"].each do |store| + use(store, ActionController::Base.session_options, + :if => lambda { + if session_store = ActionController::Base.session_store + session_store.name == store + end + } + ) +end + +use "ActionDispatch::RewindableInput" +use "ActionDispatch::ParamsParser" +use "Rack::MethodOverride" diff --git a/actionpack/lib/action_controller/dispatch/params_parser.rb b/actionpack/lib/action_controller/dispatch/params_parser.rb deleted file mode 100644 index d269fe07fa..0000000000 --- a/actionpack/lib/action_controller/dispatch/params_parser.rb +++ /dev/null @@ -1,71 +0,0 @@ -module ActionController - class ParamsParser - ActionController::Base.param_parsers[Mime::XML] = :xml_simple - ActionController::Base.param_parsers[Mime::JSON] = :json - - def initialize(app) - @app = app - end - - def call(env) - if params = parse_formatted_parameters(env) - env["action_controller.request.request_parameters"] = params - end - - @app.call(env) - end - - private - def parse_formatted_parameters(env) - request = Request.new(env) - - return false if request.content_length.zero? - - mime_type = content_type_from_legacy_post_data_format_header(env) || request.content_type - strategy = ActionController::Base.param_parsers[mime_type] - - return false unless strategy - - case strategy - when Proc - strategy.call(request.raw_post) - when :xml_simple, :xml_node - body = request.raw_post - body.blank? ? {} : Hash.from_xml(body).with_indifferent_access - when :yaml - YAML.load(request.raw_post) - when :json - body = request.raw_post - if body.blank? - {} - else - data = ActiveSupport::JSON.decode(body) - data = {:_json => data} unless data.is_a?(Hash) - data.with_indifferent_access - end - else - false - end - rescue Exception => e # YAML, XML or Ruby code block errors - raise - { "body" => request.raw_post, - "content_type" => request.content_type, - "content_length" => request.content_length, - "exception" => "#{e.message} (#{e.class})", - "backtrace" => e.backtrace } - end - - def content_type_from_legacy_post_data_format_header(env) - if x_post_format = env['HTTP_X_POST_DATA_FORMAT'] - case x_post_format.to_s.downcase - when 'yaml' - return Mime::YAML - when 'xml' - return Mime::XML - end - end - - nil - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/rack/failsafe.rb b/actionpack/lib/action_controller/dispatch/rack/failsafe.rb deleted file mode 100644 index 567581142c..0000000000 --- a/actionpack/lib/action_controller/dispatch/rack/failsafe.rb +++ /dev/null @@ -1,52 +0,0 @@ -module ActionController - class Failsafe - cattr_accessor :error_file_path - self.error_file_path = Rails.public_path if defined?(Rails.public_path) - - def initialize(app) - @app = app - end - - def call(env) - @app.call(env) - rescue Exception => exception - # Reraise exception in test environment - if env["rack.test"] - raise exception - else - failsafe_response(exception) - end - end - - private - def failsafe_response(exception) - log_failsafe_exception(exception) - [500, {'Content-Type' => 'text/html'}, failsafe_response_body] - rescue Exception => failsafe_error # Logger or IO errors - $stderr.puts "Error during failsafe response: #{failsafe_error}" - end - - def failsafe_response_body - error_path = "#{self.class.error_file_path}/500.html" - if File.exist?(error_path) - File.read(error_path) - else - "

500 Internal Server Error

" - end - end - - def log_failsafe_exception(exception) - message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: 500 Internal Server Error\n" - message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception - failsafe_logger.fatal(message) - end - - def failsafe_logger - if defined?(Rails) && Rails.logger - Rails.logger - else - Logger.new($stderr) - end - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/rack/lock.rb b/actionpack/lib/action_controller/dispatch/rack/lock.rb deleted file mode 100644 index c50762216e..0000000000 --- a/actionpack/lib/action_controller/dispatch/rack/lock.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActionController - class Lock - FLAG = 'rack.multithread'.freeze - - def initialize(app, lock = Mutex.new) - @app, @lock = app, lock - end - - def call(env) - old, env[FLAG] = env[FLAG], false - @lock.synchronize { @app.call(env) } - ensure - env[FLAG] = old - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb b/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb deleted file mode 100644 index dbc2fda41e..0000000000 --- a/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb +++ /dev/null @@ -1,109 +0,0 @@ -module ActionController - class MiddlewareStack < Array - class Middleware - def self.new(klass, *args, &block) - if klass.is_a?(self) - klass - else - super - end - end - - attr_reader :args, :block - - def initialize(klass, *args, &block) - @klass = klass - - options = args.extract_options! - if options.has_key?(:if) - @conditional = options.delete(:if) - else - @conditional = true - end - args << options unless options.empty? - - @args = args - @block = block - end - - def klass - if @klass.is_a?(Class) - @klass - else - @klass.to_s.constantize - end - rescue NameError - @klass - end - - def active? - if @conditional.respond_to?(:call) - @conditional.call - else - @conditional - end - end - - def ==(middleware) - case middleware - when Middleware - klass == middleware.klass - when Class - klass == middleware - else - klass == middleware.to_s.constantize - end - end - - def inspect - str = klass.to_s - args.each { |arg| str += ", #{arg.inspect}" } - str - end - - def build(app) - if block - klass.new(app, *args, &block) - else - klass.new(app, *args) - end - end - end - - def initialize(*args, &block) - super(*args) - block.call(self) if block_given? - end - - def insert(index, *args, &block) - index = self.index(index) unless index.is_a?(Integer) - middleware = Middleware.new(*args, &block) - super(index, middleware) - end - - alias_method :insert_before, :insert - - def insert_after(index, *args, &block) - index = self.index(index) unless index.is_a?(Integer) - insert(index + 1, *args, &block) - end - - def swap(target, *args, &block) - insert_before(target, *args, &block) - delete(target) - end - - def use(*args, &block) - middleware = Middleware.new(*args, &block) - push(middleware) - end - - def active - find_all { |middleware| middleware.active? } - end - - def build(app) - active.reverse.inject(app) { |a, e| e.build(a) } - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/rack/middlewares.rb b/actionpack/lib/action_controller/dispatch/rack/middlewares.rb deleted file mode 100644 index f9cfc2b18e..0000000000 --- a/actionpack/lib/action_controller/dispatch/rack/middlewares.rb +++ /dev/null @@ -1,21 +0,0 @@ -use "Rack::Lock", :if => lambda { - !ActionController::Base.allow_concurrency -} - -use "ActionController::Failsafe" - -["ActionController::Session::CookieStore", - "ActionController::Session::MemCacheStore", - "ActiveRecord::SessionStore"].each do |store| - use(store, ActionController::Base.session_options, - :if => lambda { - if session_store = ActionController::Base.session_store - session_store.name == store - end - } - ) -end - -use "ActionController::RewindableInput" -use "ActionController::ParamsParser" -use "Rack::MethodOverride" diff --git a/actionpack/lib/action_controller/dispatch/request.rb b/actionpack/lib/action_controller/dispatch/request.rb deleted file mode 100755 index f8c77241b9..0000000000 --- a/actionpack/lib/action_controller/dispatch/request.rb +++ /dev/null @@ -1,492 +0,0 @@ -require 'tempfile' -require 'stringio' -require 'strscan' - -require 'active_support/memoizable' -require 'action_controller/cgi_ext' - -module ActionController - class Request < Rack::Request - extend ActiveSupport::Memoizable - - %w[ AUTH_TYPE GATEWAY_INTERFACE - PATH_TRANSLATED REMOTE_HOST - REMOTE_IDENT REMOTE_USER REMOTE_ADDR - SERVER_NAME SERVER_PROTOCOL - - HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING - HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM - HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env| - define_method(env.sub(/^HTTP_/n, '').downcase) do - @env[env] - end - end - - def key?(key) - @env.key?(key) - end - - HTTP_METHODS = %w(get head put post delete options) - HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } - - # Returns the true HTTP request \method as a lowercase symbol, such as - # :get. If the request \method is not listed in the HTTP_METHODS - # constant above, an UnknownHttpMethod exception is raised. - def request_method - HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") - end - memoize :request_method - - # Returns the HTTP request \method used for action processing as a - # lowercase symbol, such as :post. (Unlike #request_method, this - # method returns :get for a HEAD request because the two are - # functionally equivalent from the application's perspective.) - def method - request_method == :head ? :get : request_method - end - - # Is this a GET (or HEAD) request? Equivalent to request.method == :get. - def get? - method == :get - end - - # Is this a POST request? Equivalent to request.method == :post. - def post? - request_method == :post - end - - # Is this a PUT request? Equivalent to request.method == :put. - def put? - request_method == :put - end - - # Is this a DELETE request? Equivalent to request.method == :delete. - def delete? - request_method == :delete - end - - # Is this a HEAD request? Since request.method sees HEAD as :get, - # this \method checks the actual HTTP \method directly. - def head? - request_method == :head - end - - # Provides access to the request's HTTP headers, for example: - # - # request.headers["Content-Type"] # => "text/plain" - def headers - ActionController::Http::Headers.new(@env) - end - memoize :headers - - # Returns the content length of the request as an integer. - def content_length - super.to_i - end - - # The MIME type of the HTTP request, such as Mime::XML. - # - # For backward compatibility, the post \format is extracted from the - # X-Post-Data-Format HTTP header if present. - def content_type - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ - Mime::Type.lookup($1.strip.downcase) - else - nil - end - end - memoize :content_type - - # Returns the accepted MIME type for the request. - def accepts - header = @env['HTTP_ACCEPT'].to_s.strip - - fallback = xhr? ? Mime::JS : Mime::HTML - - if header.empty? - [content_type, fallback, Mime::ALL].compact - else - ret = Mime::Type.parse(header) - if ret.last == Mime::ALL - ret.insert(-2, fallback) - end - ret - end - end - memoize :accepts - - def if_modified_since - if since = env['HTTP_IF_MODIFIED_SINCE'] - Time.rfc2822(since) rescue nil - end - end - memoize :if_modified_since - - def if_none_match - env['HTTP_IF_NONE_MATCH'] - 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 - end - - # Check response freshness (Last-Modified and ETag) against request - # If-Modified-Since and If-None-Match conditions. If both headers are - # supplied, both must match, or the request is not considered fresh. - def fresh?(response) - case - when if_modified_since && if_none_match - not_modified?(response.last_modified) && etag_matches?(response.etag) - when if_modified_since - not_modified?(response.last_modified) - when if_none_match - etag_matches?(response.etag) - else - false - end - end - - ONLY_ALL = [Mime::ALL].freeze - - # Returns the Mime type for the \format used in the request. - # - # GET /posts/5.xml | request.format => Mime::XML - # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of ActionController::Base.use_accept_header - - def format(view_path = []) - @format ||= - if parameters[:format] - Mime[parameters[:format]] - elsif Base.use_accept_header && !(accepts == ONLY_ALL) - accepts.first - elsif xhr? then Mime::JS - else Mime::HTML - end - end - - def formats - @formats = - if Base.use_accept_header - ret = Array(Mime[parameters[:format]] || accepts) - else - [format] - end - end - - # Sets the \format by string extension, which can be used to force custom formats - # that are not controlled by the extension. - # - # class ApplicationController < ActionController::Base - # before_filter :adjust_format_for_iphone - # - # private - # def adjust_format_for_iphone - # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] - # end - # end - def format=(extension) - parameters[:format] = extension.to_s - @format = Mime::Type.lookup_by_extension(parameters[:format]) - end - - # Returns a symbolized version of the :format parameter of the request. - # If no \format is given it returns :jsfor Ajax requests and :html - # otherwise. - def template_format - parameter_format = parameters[:format] - - if parameter_format - parameter_format - elsif xhr? - :js - else - :html - end - end - - def cache_format - parameters[:format] - end - - # Returns true if the request's "X-Requested-With" header contains - # "XMLHttpRequest". (The Prototype Javascript library sends this header with - # every Ajax request.) - def xml_http_request? - !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i) - end - alias xhr? :xml_http_request? - - # Which IP addresses are "trusted proxies" that can be stripped from - # the right-hand-side of X-Forwarded-For - TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i - - # Determines originating IP address. REMOTE_ADDR is the standard - # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or - # HTTP_X_FORWARDED_FOR are set by proxies so check for these if - # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- - # delimited list in the case of multiple chained proxies; the last - # address which is not trusted is the originating IP. - def remote_ip - remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/) - - unless remote_addr_list.blank? - not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES} - return not_trusted_addrs.first unless not_trusted_addrs.empty? - end - remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',') - - if @env.include? 'HTTP_CLIENT_IP' - if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP']) - # We don't know which came from the proxy, and which from the user - raise ActionControllerError.new(< 1 && TRUSTED_PROXIES =~ remote_ips.last.strip - remote_ips.pop - end - - return remote_ips.last.strip - end - - @env['REMOTE_ADDR'] - end - memoize :remote_ip - - # Returns the lowercase name of the HTTP server software. - def server_software - (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil - end - memoize :server_software - - # Returns the complete URL used for this request. - def url - protocol + host_with_port + request_uri - end - memoize :url - - # Returns 'https://' if this is an SSL request and 'http://' otherwise. - def protocol - ssl? ? 'https://' : 'http://' - end - memoize :protocol - - # Is this an SSL request? - def ssl? - @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' - end - - # Returns the \host for this request, such as "example.com". - def raw_host_with_port - if forwarded = env["HTTP_X_FORWARDED_HOST"] - forwarded.split(/,\s?/).last - else - env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" - end - end - - # Returns the host for this request, such as example.com. - def host - raw_host_with_port.sub(/:\d+$/, '') - end - memoize :host - - # Returns a \host:\port string for this request, such as "example.com" or - # "example.com:8080". - def host_with_port - "#{host}#{port_string}" - end - memoize :host_with_port - - # Returns the port number of this request as an integer. - def port - if raw_host_with_port =~ /:(\d+)$/ - $1.to_i - else - standard_port - end - end - memoize :port - - # Returns the standard \port number for this request's protocol. - def standard_port - case protocol - when 'https://' then 443 - else 80 - end - end - - # Returns a \port suffix like ":8080" if the \port number of this request - # is not the default HTTP \port 80 or HTTPS \port 443. - def port_string - port == standard_port ? '' : ":#{port}" - end - - # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify - # a different tld_length, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". - def domain(tld_length = 1) - return nil unless named_host?(host) - - host.split('.').last(1 + tld_length).join('.') - end - - # Returns all the \subdomains as an array, so ["dev", "www"] would be - # returned for "dev.www.rubyonrails.org". You can specify a different tld_length, - # such as 2 to catch ["www"] instead of ["www", "rubyonrails"] - # in "www.rubyonrails.co.uk". - def subdomains(tld_length = 1) - return [] unless named_host?(host) - parts = host.split('.') - parts[0..-(tld_length+2)] - end - - # Returns the query string, accounting for server idiosyncrasies. - def query_string - @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') - end - memoize :query_string - - # Returns the request URI, accounting for server idiosyncrasies. - # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. - def request_uri - if uri = @env['REQUEST_URI'] - # Remove domain, which webrick puts into the request_uri. - (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri - else - # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. - uri = @env['PATH_INFO'].to_s - - if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) - uri = uri.sub(/#{script_filename}\//, '') - end - - env_qs = @env['QUERY_STRING'].to_s - uri += "?#{env_qs}" unless env_qs.empty? - - if uri.blank? - @env.delete('REQUEST_URI') - else - @env['REQUEST_URI'] = uri - end - end - end - memoize :request_uri - - # Returns the interpreted \path to requested resource after all the installation - # directory of this application was taken into account. - def path - path = request_uri.to_s[/\A[^\?]*/] - path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') - path - end - memoize :path - - # Read the request \body. This is useful for web services that need to - # work with raw requests directly. - def raw_post - unless @env.include? 'RAW_POST_DATA' - @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i) - body.rewind if body.respond_to?(:rewind) - end - @env['RAW_POST_DATA'] - end - - # Returns both GET and POST \parameters in a single hash. - def parameters - @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access - end - alias_method :params, :parameters - - def path_parameters=(parameters) #:nodoc: - @env["rack.routing_args"] = parameters - @symbolized_path_parameters = @parameters = nil - end - - # The same as path_parameters with explicitly symbolized keys. - def symbolized_path_parameters - @symbolized_path_parameters ||= path_parameters.symbolize_keys - end - - # Returns a hash with the \parameters used to form the \path of the request. - # Returned hash keys are strings: - # - # {'action' => 'my_action', 'controller' => 'my_controller'} - # - # See symbolized_path_parameters for symbolized keys. - def path_parameters - @env["rack.routing_args"] ||= {} - end - - # The request body is an IO input stream. If the RAW_POST_DATA environment - # variable is already set, wrap it in a StringIO. - def body - if raw_post = @env['RAW_POST_DATA'] - raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) - StringIO.new(raw_post) - else - @env['rack.input'] - end - end - - def form_data? - FORM_DATA_MEDIA_TYPES.include?(content_type.to_s) - end - - # Override Rack's GET method to support nested query strings - def GET - @env["action_controller.request.query_parameters"] ||= UrlEncodedPairParser.parse_query_parameters(query_string) - end - alias_method :query_parameters, :GET - - # Override Rack's POST method to support nested query strings - def POST - @env["action_controller.request.request_parameters"] ||= UrlEncodedPairParser.parse_hash_parameters(super) - end - alias_method :request_parameters, :POST - - def body_stream #:nodoc: - @env['rack.input'] - end - - def session - @env['rack.session'] ||= {} - end - - def session=(session) #:nodoc: - @env['rack.session'] = session - end - - def reset_session - @env['rack.session'] = {} - end - - def session_options - @env['rack.session.options'] ||= {} - end - - def session_options=(options) - @env['rack.session.options'] = options - end - - def server_port - @env['SERVER_PORT'].to_i - end - - private - def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/request_parser.rb b/actionpack/lib/action_controller/dispatch/request_parser.rb deleted file mode 100644 index d1739ef4d0..0000000000 --- a/actionpack/lib/action_controller/dispatch/request_parser.rb +++ /dev/null @@ -1,315 +0,0 @@ -module ActionController - class RequestParser - def initialize(env) - @env = env - freeze - end - - def request_parameters - @env["action_controller.request_parser.request_parameters"] ||= parse_formatted_request_parameters - end - - def query_parameters - @env["action_controller.request_parser.query_parameters"] ||= self.class.parse_query_parameters(query_string) - end - - # Returns the query string, accounting for server idiosyncrasies. - def query_string - @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') - end - - # The request body is an IO input stream. If the RAW_POST_DATA environment - # variable is already set, wrap it in a StringIO. - def body - if raw_post = @env['RAW_POST_DATA'] - raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) - StringIO.new(raw_post) - else - @env['rack.input'] - end - end - - # The raw content type string with its parameters stripped off. - def content_type_without_parameters - self.class.extract_content_type_without_parameters(content_type_with_parameters) - end - - def raw_post - unless @env.include? 'RAW_POST_DATA' - @env['RAW_POST_DATA'] = body.read(content_length) - body.rewind if body.respond_to?(:rewind) - end - @env['RAW_POST_DATA'] - end - - private - - def parse_formatted_request_parameters - return {} if content_length.zero? - - content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters) - - # Don't parse params for unknown requests. - return {} if content_type.blank? - - mime_type = Mime::Type.lookup(content_type) - strategy = ActionController::Base.param_parsers[mime_type] - - # Only multipart form parsing expects a stream. - body = (strategy && strategy != :multipart_form) ? raw_post : self.body - - case strategy - when Proc - strategy.call(body) - when :url_encoded_form - self.class.clean_up_ajax_request_body! body - self.class.parse_query_parameters(body) - when :multipart_form - self.class.parse_multipart_form_parameters(body, boundary, content_length, @env) - when :xml_simple, :xml_node - body.blank? ? {} : Hash.from_xml(body).with_indifferent_access - when :yaml - YAML.load(body) - when :json - if body.blank? - {} - else - data = ActiveSupport::JSON.decode(body) - data = {:_json => data} unless data.is_a?(Hash) - data.with_indifferent_access - end - else - {} - end - rescue Exception => e # YAML, XML or Ruby code block errors - raise - { "body" => body, - "content_type" => content_type_with_parameters, - "content_length" => content_length, - "exception" => "#{e.message} (#{e.class})", - "backtrace" => e.backtrace } - end - - def content_length - @env['CONTENT_LENGTH'].to_i - end - - # The raw content type string. Use when you need parameters such as - # charset or boundary which aren't included in the content_type MIME type. - # Overridden by the X-POST_DATA_FORMAT header for backward compatibility. - def content_type_with_parameters - content_type_from_legacy_post_data_format_header || @env['CONTENT_TYPE'].to_s - end - - def content_type_from_legacy_post_data_format_header - if x_post_format = @env['HTTP_X_POST_DATA_FORMAT'] - case x_post_format.to_s.downcase - when 'yaml'; 'application/x-yaml' - when 'xml'; 'application/xml' - end - end - end - - class << self - def parse_query_parameters(query_string) - return {} if query_string.blank? - - pairs = query_string.split('&').collect do |chunk| - next if chunk.empty? - key, value = chunk.split('=', 2) - next if key.empty? - value = value.nil? ? nil : CGI.unescape(value) - [ CGI.unescape(key), value ] - end.compact - - UrlEncodedPairParser.new(pairs).result - end - - def parse_request_parameters(params) - parser = UrlEncodedPairParser.new - - params = params.dup - until params.empty? - for key, value in params - if key.blank? - params.delete key - elsif !key.include?('[') - # much faster to test for the most common case first (GET) - # and avoid the call to build_deep_hash - parser.result[key] = get_typed_value(value[0]) - params.delete key - elsif value.is_a?(Array) - parser.parse(key, get_typed_value(value.shift)) - params.delete key if value.empty? - else - raise TypeError, "Expected array, found #{value.inspect}" - end - end - end - - parser.result - end - - def parse_multipart_form_parameters(body, boundary, body_size, env) - parse_request_parameters(read_multipart(body, boundary, body_size, env)) - end - - def extract_multipart_boundary(content_type_with_parameters) - if content_type_with_parameters =~ MULTIPART_BOUNDARY - ['multipart/form-data', $1.dup] - else - extract_content_type_without_parameters(content_type_with_parameters) - end - end - - def extract_content_type_without_parameters(content_type_with_parameters) - $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/ - end - - def clean_up_ajax_request_body!(body) - body.chop! if body[-1] == 0 - body.gsub!(/&_=$/, '') - end - - - private - def get_typed_value(value) - case value - when String - value - when NilClass - '' - when Array - value.map { |v| get_typed_value(v) } - else - if value.respond_to? :original_filename - # Uploaded file - if value.original_filename - value - # Multipart param - else - result = value.read - value.rewind - result - end - # Unknown value, neither string nor multipart. - else - raise "Unknown form value: #{value.inspect}" - end - end - end - - MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n - - EOL = "\015\012" - - def read_multipart(body, boundary, body_size, env) - params = Hash.new([]) - boundary = "--" + boundary - quoted_boundary = Regexp.quote(boundary) - buf = "" - bufsize = 10 * 1024 - boundary_end="" - - # start multipart/form-data - body.binmode if defined? body.binmode - case body - when File - body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding) - when StringIO - body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding) - end - boundary_size = boundary.size + EOL.size - body_size -= boundary_size - status = body.read(boundary_size) - if nil == status - raise EOFError, "no content body" - elsif boundary + EOL != status - raise EOFError, "bad content body" - end - - loop do - head = nil - content = - if 10240 < body_size - UploadedTempfile.new("CGI") - else - UploadedStringIO.new - end - content.binmode if defined? content.binmode - - until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf) - - if (not head) and /#{EOL}#{EOL}/n.match(buf) - buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do - head = $1.dup - "" - end - next - end - - if head and ( (EOL + boundary + EOL).size < buf.size ) - content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)] - buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = "" - end - - c = if bufsize < body_size - body.read(bufsize) - else - body.read(body_size) - end - if c.nil? || c.empty? - raise EOFError, "bad content body" - end - buf.concat(c) - body_size -= c.size - end - - buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do - content.print $1 - if "--" == $2 - body_size = -1 - end - boundary_end = $2.dup - "" - end - - content.rewind - - head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni - if filename = $1 || $2 - if /Mac/ni.match(env['HTTP_USER_AGENT']) and - /Mozilla/ni.match(env['HTTP_USER_AGENT']) and - (not /MSIE/ni.match(env['HTTP_USER_AGENT'])) - filename = CGI.unescape(filename) - end - content.original_path = filename.dup - end - - head =~ /Content-Type: ([^\r]*)/ni - content.content_type = $1.dup if $1 - - head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni - name = $1.dup if $1 - - if params.has_key?(name) - params[name].push(content) - else - params[name] = [content] - end - break if body_size == -1 - end - raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/ - - begin - body.rewind if body.respond_to?(:rewind) - rescue Errno::ESPIPE - # Handles exceptions raised by input streams that cannot be rewound - # such as when using plain CGI under Apache - end - - params - end - end # class << self - end -end diff --git a/actionpack/lib/action_controller/dispatch/rescue.rb b/actionpack/lib/action_controller/dispatch/rescue.rb index 0293e62fc7..df0a976204 100644 --- a/actionpack/lib/action_controller/dispatch/rescue.rb +++ b/actionpack/lib/action_controller/dispatch/rescue.rb @@ -60,8 +60,8 @@ module ActionController #:nodoc: module ClassMethods def call_with_exception(env, exception) #:nodoc: - request = env["action_controller.rescue.request"] ||= ActionController::Request.new(env) - response = env["action_controller.rescue.response"] ||= ActionController::Response.new + request = env["action_controller.rescue.request"] ||= ActionDispatch::Request.new(env) + response = env["action_controller.rescue.response"] ||= ActionDispatch::Response.new new.process(request, response, :rescue_action, exception) end end diff --git a/actionpack/lib/action_controller/dispatch/response.rb b/actionpack/lib/action_controller/dispatch/response.rb deleted file mode 100644 index 27860a6207..0000000000 --- a/actionpack/lib/action_controller/dispatch/response.rb +++ /dev/null @@ -1,255 +0,0 @@ -require 'digest/md5' - -module ActionController # :nodoc: - # Represents an HTTP response generated by a controller action. One can use - # an ActionController::Response object to retrieve the current state - # of the response, or customize the response. An Response object can - # either represent a "real" HTTP response (i.e. one that is meant to be sent - # back to the web browser) or a test response (i.e. one that is generated - # from integration tests). See CgiResponse and TestResponse, respectively. - # - # Response is mostly a Ruby on Rails framework implement detail, and - # should never be used directly in controllers. Controllers should use the - # methods defined in ActionController::Base instead. For example, if you want - # to set the HTTP response's content MIME type, then use - # ActionControllerBase#headers instead of Response#headers. - # - # Nevertheless, integration tests may want to inspect controller responses in - # more detail, and that's when Response can be useful for application - # developers. Integration test methods such as - # ActionController::Integration::Session#get and - # ActionController::Integration::Session#post return objects of type - # TestResponse (which are of course also of type Response). - # - # For example, the following demo integration "test" prints the body of the - # controller response to the console: - # - # class DemoControllerTest < ActionController::IntegrationTest - # def test_print_root_path_to_console - # get('/') - # puts @response.body - # end - # end - class Response < Rack::Response - DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } - attr_accessor :request - - attr_accessor :session, :assigns, :template, :layout - attr_accessor :redirected_to, :redirected_to_method_params - - delegate :default_charset, :to => 'ActionController::Base' - - def initialize - @status = 200 - @header = DEFAULT_HEADERS.dup - - @writer = lambda { |x| @body << x } - @block = nil - - @body = "", - @session, @assigns = [], [] - end - - def location; headers['Location'] end - def location=(url) headers['Location'] = url end - - - # Sets the HTTP response's content MIME type. For example, in the controller - # you could write this: - # - # response.content_type = "text/plain" - # - # 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. - def content_type=(mime_type) - self.headers["Content-Type"] = - if mime_type =~ /charset/ || (c = charset).nil? - mime_type.to_s - else - "#{mime_type}; charset=#{c}" - end - end - - # Returns the response's content MIME type, or nil if content type has been set. - def content_type - content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0] - content_type.blank? ? nil : content_type - end - - # Set the charset of the Content-Type header. Set to nil to remove it. - # If no content type is set, it defaults to HTML. - def charset=(charset) - headers["Content-Type"] = - if charset - "#{content_type || Mime::HTML}; charset=#{charset}" - else - content_type || Mime::HTML.to_s - end - end - - def charset - charset = String(headers["Content-Type"] || headers["type"]).split(";")[1] - charset.blank? ? nil : charset.strip.split("=")[1] - end - - def last_modified - if last = headers['Last-Modified'] - Time.httpdate(last) - end - end - - def last_modified? - headers.include?('Last-Modified') - end - - def last_modified=(utc_time) - headers['Last-Modified'] = utc_time.httpdate - end - - def etag - headers['ETag'] - end - - def etag? - headers.include?('ETag') - end - - def etag=(etag) - if etag.blank? - headers.delete('ETag') - else - headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}") - end - end - - def redirect(url, status) - self.status = status - self.location = url.gsub(/[\r\n]/, '') - self.body = "You are being redirected." - end - - def sending_file? - headers["Content-Transfer-Encoding"] == "binary" - end - - def assign_default_content_type_and_charset! - self.content_type ||= Mime::HTML - self.charset ||= default_charset unless sending_file? - end - - def prepare! - assign_default_content_type_and_charset! - handle_conditional_get! - set_content_length! - convert_content_type! - convert_language! - convert_expires! - convert_cookies! - end - - def each(&callback) - if @body.respond_to?(:call) - @writer = lambda { |x| callback.call(x) } - @body.call(self, self) - elsif @body.is_a?(String) - @body.each_line(&callback) - else - @body.each(&callback) - end - - @writer = callback - @block.call(self) if @block - end - - def write(str) - @writer.call str.to_s - str - end - - # Over Rack::Response#set_cookie to add HttpOnly option - def set_cookie(key, value) - case value - when Hash - domain = "; domain=" + value[:domain] if value[:domain] - path = "; path=" + value[:path] if value[:path] - # According to RFC 2109, we need dashes here. - # N.B.: cgi.rb uses spaces... - expires = "; expires=" + value[:expires].clone.gmtime. - strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] - secure = "; secure" if value[:secure] - httponly = "; HttpOnly" if value[:http_only] - value = value[:value] - end - value = [value] unless Array === value - cookie = ::Rack::Utils.escape(key) + "=" + - value.map { |v| ::Rack::Utils.escape v }.join("&") + - "#{domain}#{path}#{expires}#{secure}#{httponly}" - - case self["Set-Cookie"] - when Array - self["Set-Cookie"] << cookie - when String - self["Set-Cookie"] = [self["Set-Cookie"], cookie] - when nil - self["Set-Cookie"] = cookie - end - end - - private - def handle_conditional_get! - if etag? || last_modified? - set_conditional_cache_control! - elsif nonempty_ok_response? - self.etag = body - - if request && request.etag_matches?(etag) - self.status = '304 Not Modified' - self.body = '' - end - - set_conditional_cache_control! - end - end - - def nonempty_ok_response? - ok = !status || status.to_s[0..2] == '200' - ok && body.is_a?(String) && !body.empty? - end - - def set_conditional_cache_control! - if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control'] - headers['Cache-Control'] = 'private, max-age=0, must-revalidate' - end - end - - def convert_content_type! - headers['Content-Type'] ||= "text/html" - headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset'] - end - - # Don't set the Content-Length for block-based bodies as that would mean - # reading it all into memory. Not nice for, say, a 2GB streaming file. - def set_content_length! - if status && status.to_s[0..2] == '204' - headers.delete('Content-Length') - elsif length = headers['Content-Length'] - headers['Content-Length'] = length.to_s - elsif !body.respond_to?(:call) && (!status || status.to_s[0..2] != '304') - headers["Content-Length"] = body.size.to_s - end - end - - def convert_language! - headers["Content-Language"] = headers.delete("language") if headers["language"] - end - - def convert_expires! - headers["Expires"] = headers.delete("") if headers["expires"] - end - - def convert_cookies! - headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/rewindable_input.rb b/actionpack/lib/action_controller/dispatch/rewindable_input.rb deleted file mode 100644 index 36f655c51e..0000000000 --- a/actionpack/lib/action_controller/dispatch/rewindable_input.rb +++ /dev/null @@ -1,28 +0,0 @@ -module ActionController - class RewindableInput - class RewindableIO < ActiveSupport::BasicObject - def initialize(io) - @io = io - @rewindable = io.is_a?(StringIO) - end - - def method_missing(method, *args, &block) - unless @rewindable - @io = StringIO.new(@io.read) - @rewindable = true - end - - @io.__send__(method, *args, &block) - end - end - - def initialize(app) - @app = app - end - - def call(env) - env['rack.input'] = RewindableIO.new(env['rack.input']) - @app.call(env) - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/status_codes.rb b/actionpack/lib/action_controller/dispatch/status_codes.rb deleted file mode 100644 index 4977c79491..0000000000 --- a/actionpack/lib/action_controller/dispatch/status_codes.rb +++ /dev/null @@ -1,88 +0,0 @@ -module ActionController - module StatusCodes #:nodoc: - # Defines the standard HTTP status codes, by integer, with their - # corresponding default message texts. - # Source: http://www.iana.org/assignments/http-status-codes - STATUS_CODES = { - 100 => "Continue", - 101 => "Switching Protocols", - 102 => "Processing", - - 200 => "OK", - 201 => "Created", - 202 => "Accepted", - 203 => "Non-Authoritative Information", - 204 => "No Content", - 205 => "Reset Content", - 206 => "Partial Content", - 207 => "Multi-Status", - 226 => "IM Used", - - 300 => "Multiple Choices", - 301 => "Moved Permanently", - 302 => "Found", - 303 => "See Other", - 304 => "Not Modified", - 305 => "Use Proxy", - 307 => "Temporary Redirect", - - 400 => "Bad Request", - 401 => "Unauthorized", - 402 => "Payment Required", - 403 => "Forbidden", - 404 => "Not Found", - 405 => "Method Not Allowed", - 406 => "Not Acceptable", - 407 => "Proxy Authentication Required", - 408 => "Request Timeout", - 409 => "Conflict", - 410 => "Gone", - 411 => "Length Required", - 412 => "Precondition Failed", - 413 => "Request Entity Too Large", - 414 => "Request-URI Too Long", - 415 => "Unsupported Media Type", - 416 => "Requested Range Not Satisfiable", - 417 => "Expectation Failed", - 422 => "Unprocessable Entity", - 423 => "Locked", - 424 => "Failed Dependency", - 426 => "Upgrade Required", - - 500 => "Internal Server Error", - 501 => "Not Implemented", - 502 => "Bad Gateway", - 503 => "Service Unavailable", - 504 => "Gateway Timeout", - 505 => "HTTP Version Not Supported", - 507 => "Insufficient Storage", - 510 => "Not Extended" - } - - # Provides a symbol-to-fixnum lookup for converting a symbol (like - # :created or :not_implemented) into its corresponding HTTP status - # code (like 200 or 501). - SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)| - hash[message.gsub(/ /, "").underscore.to_sym] = code - hash - end - - # Given a status parameter, determine whether it needs to be converted - # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup - # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE - # hash to convert it. - def interpret_status(status) - case status - when Fixnum then - "#{status} #{STATUS_CODES[status]}".strip - when Symbol then - interpret_status(SYMBOL_TO_STATUS_CODE[status] || - "500 Unknown Status #{status.inspect}") - else - status.to_s - end - end - private :interpret_status - - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/dispatch/uploaded_file.rb b/actionpack/lib/action_controller/dispatch/uploaded_file.rb deleted file mode 100644 index 376ba3621a..0000000000 --- a/actionpack/lib/action_controller/dispatch/uploaded_file.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActionController - module UploadedFile - def self.included(base) - base.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path - end - end - - def self.extended(object) - object.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path - end - end - - # Take the basename of the upload's original filename. - # This handles the full Windows paths given by Internet Explorer - # (and perhaps other broken user agents) without affecting - # those which give the lone filename. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - unless defined? @original_filename - @original_filename = - unless original_path.blank? - if original_path =~ /^(?:.*[:\\\/])?(.*)/m - $1 - else - File.basename original_path - end - end - end - @original_filename - end - end - - class UploadedStringIO < StringIO - include UploadedFile - end - - class UploadedTempfile < Tempfile - include UploadedFile - end -end diff --git a/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb b/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb deleted file mode 100644 index 57594c4259..0000000000 --- a/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb +++ /dev/null @@ -1,155 +0,0 @@ -module ActionController - class UrlEncodedPairParser < StringScanner #:nodoc: - class << self - def parse_query_parameters(query_string) - return {} if query_string.blank? - - pairs = query_string.split('&').collect do |chunk| - next if chunk.empty? - key, value = chunk.split('=', 2) - next if key.empty? - value = value.nil? ? nil : CGI.unescape(value) - [ CGI.unescape(key), value ] - end.compact - - new(pairs).result - end - - def parse_hash_parameters(params) - parser = new - - params = params.dup - until params.empty? - for key, value in params - if key.blank? - params.delete(key) - elsif value.is_a?(Array) - parser.parse(key, get_typed_value(value.shift)) - params.delete(key) if value.empty? - else - parser.parse(key, get_typed_value(value)) - params.delete(key) - end - end - end - - parser.result - end - - private - def get_typed_value(value) - case value - when String - value - when NilClass - '' - when Array - value.map { |v| get_typed_value(v) } - when Hash - if value.has_key?(:tempfile) && value[:filename].any? - upload = value[:tempfile] - upload.extend(UploadedFile) - upload.original_path = value[:filename] - upload.content_type = value[:type] - upload - else - nil - end - else - raise "Unknown form value: #{value.inspect}" - end - end - end - - attr_reader :top, :parent, :result - - def initialize(pairs = []) - super('') - @result = {} - pairs.each { |key, value| parse(key, value) } - end - - KEY_REGEXP = %r{([^\[\]=&]+)} - BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} - - # Parse the query string - def parse(key, value) - self.string = key - @top, @parent = result, nil - - # First scan the bare key - key = scan(KEY_REGEXP) or return - key = post_key_check(key) - - # Then scan as many nestings as present - until eos? - r = scan(BRACKETED_KEY_REGEXP) or return - key = self[1] - key = post_key_check(key) - end - - bind(key, value) - end - - private - # After we see a key, we must look ahead to determine our next action. Cases: - # - # [] follows the key. Then the value must be an array. - # = follows the key. (A value comes next) - # & or the end of string follows the key. Then the key is a flag. - # otherwise, a hash follows the key. - def post_key_check(key) - if scan(/\[\]/) # a[b][] indicates that b is an array - container(key, Array) - nil - elsif check(/\[[^\]]/) # a[b] indicates that a is a hash - container(key, Hash) - nil - else # End of key? We do nothing. - key - end - end - - # Add a container to the stack. - def container(key, klass) - type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) - value = bind(key, klass.new) - type_conflict! klass, value unless value.is_a?(klass) - push(value) - end - - # Push a value onto the 'stack', which is actually only the top 2 items. - def push(value) - @parent, @top = @top, value - end - - # Bind a key (which may be nil for items in an array) to the provided value. - def bind(key, value) - if top.is_a? Array - if key - if top[-1].is_a?(Hash) && ! top[-1].key?(key) - top[-1][key] = value - else - top << {key => value}.with_indifferent_access - end - push top.last - return top[key] - else - top << value - return value - end - elsif top.is_a? Hash - key = CGI.unescape(key) - parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) - top[key] ||= value - return top[key] - else - raise ArgumentError, "Don't know what to do: top is #{top.inspect}" - end - end - - def type_conflict!(klass, value) - raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" - end - end -end diff --git a/actionpack/lib/action_controller/mime/default_types.rb b/actionpack/lib/action_controller/mime/default_types.rb deleted file mode 100644 index 2d7fba1173..0000000000 --- a/actionpack/lib/action_controller/mime/default_types.rb +++ /dev/null @@ -1,21 +0,0 @@ -# Build list of Mime types for HTTP responses -# http://www.iana.org/assignments/media-types/ - -Mime::Type.register "*/*", :all -Mime::Type.register "text/plain", :text, [], %w(txt) -Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml ) -Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript ) -Mime::Type.register "text/css", :css -Mime::Type.register "text/calendar", :ics -Mime::Type.register "text/csv", :csv -Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) -Mime::Type.register "application/rss+xml", :rss -Mime::Type.register "application/atom+xml", :atom -Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ) - -Mime::Type.register "multipart/form-data", :multipart_form -Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form - -# http://www.ietf.org/rfc/rfc4627.txt -# http://www.json.org/JSONRequest.html -Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) \ No newline at end of file diff --git a/actionpack/lib/action_controller/mime/type.rb b/actionpack/lib/action_controller/mime/type.rb deleted file mode 100644 index 23a39dff54..0000000000 --- a/actionpack/lib/action_controller/mime/type.rb +++ /dev/null @@ -1,214 +0,0 @@ -require 'set' - -module Mime - SET = [] - EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } - LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } - - def self.[](type) - Type.lookup_by_extension(type.to_s) - end - - # Encapsulates the notion of a mime type. Can be used at render time, for example, with: - # - # class PostsController < ActionController::Base - # def show - # @post = Post.find(params[:id]) - # - # 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 } - # end - # end - # end - class Type - @@html_types = Set.new [:html, :all] - cattr_reader :html_types - - # These are the content types which browsers can generate without using ajax, flash, etc - # i.e. following a link, getting an image or posting a form. CSRF protection - # only needs to protect against these types. - @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text] - cattr_reader :browser_generated_types - attr_reader :symbol - - @@unverifiable_types = Set.new [:text, :json, :csv, :xml, :rss, :atom, :yaml] - def self.unverifiable_types - ActiveSupport::Deprecation.warn("unverifiable_types is deprecated and has no effect", caller) - @@unverifiable_types - end - - # A simple helper class used in parsing the accept header - class AcceptItem #:nodoc: - attr_accessor :order, :name, :q - - def initialize(order, name, q=nil) - @order = order - @name = name.strip - q ||= 0.0 if @name == Mime::ALL # default wilcard 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 - end - - def ==(item) - name == (item.respond_to?(:name) ? item.name : item) - end - end - - class << self - def lookup(string) - LOOKUP[string] - end - - def lookup_by_extension(extension) - EXTENSION_LOOKUP[extension] - end - - # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for - # rendering different HTML versions depending on the user agent, like an iPhone. - def register_alias(string, symbol, extension_synonyms = []) - register(string, symbol, [], extension_synonyms, true) - end - - def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) - Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) } - - SET << Mime.const_get(symbol.to_s.upcase) - - ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup - ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last } - end - - def parse(accept_header) - if accept_header !~ /,/ - [Mime::Type.lookup(accept_header)] - else - # keep track of creation order to keep the subsequent sort stable - list = [] - accept_header.split(/,/).each_with_index do |header, index| - params, q = header.split(/;\s*q=/) - if params - params.strip! - list << AcceptItem.new(index, params, q) unless params.empty? - 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) - - 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 - end - idx += 1 - end - end - - list.map! { |i| Mime::Type.lookup(i.name) }.uniq! - list - end - end - end - - def initialize(string, symbol = nil, synonyms = []) - @symbol, @synonyms = symbol, synonyms - @string = string - end - - def to_s - @string - end - - def to_str - to_s - end - - def to_sym - @symbol || @string.to_sym - end - - def ===(list) - if list.is_a?(Array) - (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) } - else - super - end - end - - def ==(mime_type) - return false if mime_type.blank? - (@synonyms + [ self ]).any? do |synonym| - require "ruby-debug" - debugger if mime_type.is_a?(Array) - synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym - end - end - - def =~(mime_type) - return false if mime_type.blank? - regexp = Regexp.new(Regexp.quote(mime_type.to_s)) - (@synonyms + [ self ]).any? do |synonym| - synonym.to_s =~ regexp - end - end - - # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See - # ActionController::RequestForgeryProtection. - def verify_request? - @@browser_generated_types.include?(to_sym) - end - - def html? - @@html_types.include?(to_sym) || @string =~ /html/ - end - - private - def method_missing(method, *args) - if method.to_s =~ /(\w+)\?$/ - $1.downcase.to_sym == to_sym - else - super - end - end - end -end - -require 'action_controller/mime/default_types' diff --git a/actionpack/lib/action_controller/rack_ext.rb b/actionpack/lib/action_controller/rack_ext.rb deleted file mode 100644 index 2ba6654e3d..0000000000 --- a/actionpack/lib/action_controller/rack_ext.rb +++ /dev/null @@ -1,3 +0,0 @@ -require 'action_controller/rack_ext/lock' -require 'action_controller/rack_ext/multipart' -require 'action_controller/rack_ext/parse_query' diff --git a/actionpack/lib/action_controller/rack_ext/lock.rb b/actionpack/lib/action_controller/rack_ext/lock.rb deleted file mode 100644 index 9bf1889065..0000000000 --- a/actionpack/lib/action_controller/rack_ext/lock.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Rack - # Rack::Lock was commited to Rack core - # http://github.com/rack/rack/commit/7409b0c - # Remove this when Rack 1.0 is released - unless defined? Lock - class Lock - FLAG = 'rack.multithread'.freeze - - def initialize(app, lock = Mutex.new) - @app, @lock = app, lock - end - - def call(env) - old, env[FLAG] = env[FLAG], false - @lock.synchronize { @app.call(env) } - ensure - env[FLAG] = old - end - end - end -end diff --git a/actionpack/lib/action_controller/rack_ext/multipart.rb b/actionpack/lib/action_controller/rack_ext/multipart.rb deleted file mode 100644 index 3b142307e9..0000000000 --- a/actionpack/lib/action_controller/rack_ext/multipart.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Rack - module Utils - module Multipart - class << self - def parse_multipart_with_rewind(env) - result = parse_multipart_without_rewind(env) - - begin - env['rack.input'].rewind if env['rack.input'].respond_to?(:rewind) - rescue Errno::ESPIPE - # Handles exceptions raised by input streams that cannot be rewound - # such as when using plain CGI under Apache - end - - result - end - - alias_method_chain :parse_multipart, :rewind - end - end - end -end diff --git a/actionpack/lib/action_controller/rack_ext/parse_query.rb b/actionpack/lib/action_controller/rack_ext/parse_query.rb deleted file mode 100644 index 2f21a57770..0000000000 --- a/actionpack/lib/action_controller/rack_ext/parse_query.rb +++ /dev/null @@ -1,18 +0,0 @@ -# Rack does not automatically cleanup Safari 2 AJAX POST body -# This has not yet been commited to Rack, please +1 this ticket: -# http://rack.lighthouseapp.com/projects/22435/tickets/19 - -module Rack - module Utils - alias_method :parse_query_without_ajax_body_cleanup, :parse_query - module_function :parse_query_without_ajax_body_cleanup - - def parse_query(qs, d = '&;') - qs = qs.dup - qs.chop! if qs[-1] == 0 - qs.gsub!(/&_=$/, '') - parse_query_without_ajax_body_cleanup(qs, d) - end - module_function :parse_query - end -end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 044ace7de1..70cd1f642d 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -428,7 +428,7 @@ module ActionController end def call(env) - request = Request.new(env) + request = ActionDispatch::Request.new(env) app = Routing::Routes.recognize(request) app.call(env).to_a end diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb deleted file mode 100644 index bf09fd33c5..0000000000 --- a/actionpack/lib/action_controller/session/abstract_store.rb +++ /dev/null @@ -1,166 +0,0 @@ -require 'rack/utils' - -module ActionController - module Session - class AbstractStore - ENV_SESSION_KEY = 'rack.session'.freeze - ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze - - HTTP_COOKIE = 'HTTP_COOKIE'.freeze - SET_COOKIE = 'Set-Cookie'.freeze - - class SessionHash < Hash - def initialize(by, env) - super() - @by = by - @env = env - @loaded = false - end - - def id - load! unless @loaded - @id - end - - def session_id - ActiveSupport::Deprecation.warn( - "ActionController::Session::AbstractStore::SessionHash#session_id" + - "has been deprecated.Please use #id instead.", caller) - id - end - - def [](key) - load! unless @loaded - super - end - - def []=(key, value) - load! unless @loaded - super - end - - def to_hash - h = {}.replace(self) - h.delete_if { |k,v| v.nil? } - h - end - - def data - ActiveSupport::Deprecation.warn( - "ActionController::Session::AbstractStore::SessionHash#data" + - "has been deprecated.Please use #to_hash instead.", caller) - to_hash - end - - private - def loaded? - @loaded - end - - def load! - @id, session = @by.send(:load_session, @env) - replace(session) - @loaded = true - end - end - - DEFAULT_OPTIONS = { - :key => '_session_id', - :path => '/', - :domain => nil, - :expire_after => nil, - :secure => false, - :httponly => true, - :cookie_only => true - } - - def initialize(app, options = {}) - # Process legacy CGI options - options = options.symbolize_keys - if options.has_key?(:session_path) - options[:path] = options.delete(:session_path) - end - if options.has_key?(:session_key) - options[:key] = options.delete(:session_key) - end - if options.has_key?(:session_http_only) - options[:httponly] = options.delete(:session_http_only) - end - - @app = app - @default_options = DEFAULT_OPTIONS.merge(options) - @key = @default_options[:key] - @cookie_only = @default_options[:cookie_only] - end - - def call(env) - session = SessionHash.new(self, env) - - env[ENV_SESSION_KEY] = session - env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup - - response = @app.call(env) - - session_data = env[ENV_SESSION_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) - options = env[ENV_SESSION_OPTIONS_KEY] - - if session_data.is_a?(AbstractStore::SessionHash) - sid = session_data.id - else - sid = generate_sid - end - - unless set_session(env, sid, session_data.to_hash) - return response - end - - cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid) - cookie << "; domain=#{options[:domain]}" if options[:domain] - cookie << "; path=#{options[:path]}" if options[:path] - if options[:expire_after] - expiry = Time.now + options[:expire_after] - cookie << "; expires=#{expiry.httpdate}" - end - cookie << "; Secure" if options[:secure] - cookie << "; HttpOnly" if options[:httponly] - - headers = response[1] - case a = headers[SET_COOKIE] - when Array - a << cookie - when String - headers[SET_COOKIE] = [a, cookie] - when nil - headers[SET_COOKIE] = cookie - end - end - - response - end - - private - def generate_sid - ActiveSupport::SecureRandom.hex(16) - end - - def load_session(env) - request = Rack::Request.new(env) - sid = request.cookies[@key] - unless @cookie_only - sid ||= request.params[@key] - end - sid, session = get_session(env, sid) - [sid, session] - end - - def get_session(env, sid) - raise '#get_session needs to be implemented.' - end - - def set_session(env, sid, session_data) - raise '#set_session needs to be implemented.' - end - end - end -end diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb deleted file mode 100644 index 6ad6369950..0000000000 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ /dev/null @@ -1,222 +0,0 @@ -module ActionController - module Session - # This cookie-based session store is the Rails default. Sessions typically - # contain at most a user_id and flash message; both fit within the 4K cookie - # size limit. Cookie-based sessions are dramatically faster than the - # alternatives. - # - # If you have more than 4K of session data or don't want your data to be - # visible to the user, pick another session store. - # - # CookieOverflow is raised if you attempt to store more than 4K of data. - # - # A message digest is included with the cookie to ensure data integrity: - # a user cannot alter his +user_id+ without knowing the secret key - # included in the hash. New apps are generated with a pregenerated secret - # in config/environment.rb. Set your own for old apps you're upgrading. - # - # Session options: - # - # * :secret: An application-wide key string or block returning a - # string called per generated digest. The block is called with the - # 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: - # - # :secret => '449fe2e7daee471bffae2fd8dc02313d' - # :secret => Proc.new { User.current_user.secret_key } - # - # * :digest: The message digest algorithm used to verify session - # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL, - # such as 'MD5', 'RIPEMD160', 'SHA256', etc. - # - # To generate a secret key for an existing application, run - # "rake secret" and set the key in config/environment.rb. - # - # Note that changing digest or secret invalidates all existing sessions! - class CookieStore - # Cookies can typically store 4096 bytes. - MAX = 4096 - SECRET_MIN_LENGTH = 30 # characters - - DEFAULT_OPTIONS = { - :key => '_session_id', - :domain => nil, - :path => "/", - :expire_after => nil, - :httponly => true - }.freeze - - ENV_SESSION_KEY = "rack.session".freeze - ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze - HTTP_SET_COOKIE = "Set-Cookie".freeze - - # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end - - def initialize(app, options = {}) - # Process legacy CGI options - options = options.symbolize_keys - if options.has_key?(:session_path) - options[:path] = options.delete(:session_path) - end - if options.has_key?(:session_key) - options[:key] = options.delete(:session_key) - end - if options.has_key?(:session_http_only) - options[:httponly] = options.delete(:session_http_only) - end - - @app = app - - # The session_key option is required. - ensure_session_key(options[:key]) - @key = options.delete(:key).freeze - - # The secret option is required. - ensure_secret_secure(options[:secret]) - @secret = options.delete(:secret).freeze - - @digest = options.delete(:digest) || 'SHA1' - @verifier = verifier_for(@secret, @digest) - - @default_options = DEFAULT_OPTIONS.merge(options).freeze - - freeze - end - - def call(env) - env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = @default_options - - status, headers, body = @app.call(env) - - session_data = env[ENV_SESSION_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) - session_data = marshal(session_data.to_hash) - - raise CookieOverflow if session_data.size > MAX - - options = env[ENV_SESSION_OPTIONS_KEY] - cookie = Hash.new - cookie[:value] = session_data - unless options[:expire_after].nil? - cookie[:expires] = Time.now + options[:expire_after] - end - - cookie = build_cookie(@key, cookie.merge(options)) - case headers[HTTP_SET_COOKIE] - when Array - headers[HTTP_SET_COOKIE] << cookie - when String - headers[HTTP_SET_COOKIE] = [headers[HTTP_SET_COOKIE], cookie] - when nil - headers[HTTP_SET_COOKIE] = cookie - end - end - - [status, headers, body] - end - - private - # Should be in Rack::Utils soon - def build_cookie(key, value) - case value - when Hash - domain = "; domain=" + value[:domain] if value[:domain] - path = "; path=" + value[:path] if value[:path] - # According to RFC 2109, we need dashes here. - # N.B.: cgi.rb uses spaces... - expires = "; expires=" + value[:expires].clone.gmtime. - strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] - secure = "; secure" if value[:secure] - httponly = "; httponly" if value[:httponly] - value = value[:value] - end - value = [value] unless Array === value - cookie = Rack::Utils.escape(key) + "=" + - value.map { |v| Rack::Utils.escape(v) }.join("&") + - "#{domain}#{path}#{expires}#{secure}#{httponly}" - end - - def load_session(env) - request = Rack::Request.new(env) - session_data = request.cookies[@key] - data = unmarshal(session_data) || persistent_session_id!({}) - [data[:session_id], data] - end - - # Marshal a session hash into safe cookie data. Include an integrity hash. - def marshal(session) - @verifier.generate(persistent_session_id!(session)) - end - - # Unmarshal cookie data to a hash and verify its integrity. - def unmarshal(cookie) - persistent_session_id!(@verifier.verify(cookie)) if cookie - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil - end - - def ensure_session_key(key) - if key.blank? - raise ArgumentError, 'A key is required to write a ' + - 'cookie containing the session data. Use ' + - 'config.action_controller.session = { :key => ' + - '"_myapp_session", :secret => "some secret phrase" } in ' + - 'config/environment.rb' - end - end - - # To prevent users from using something insecure like "Password" we make sure that the - # secret they've provided is at least 30 characters in length. - def ensure_secret_secure(secret) - # There's no way we can do this check if they've provided a proc for the - # secret. - return true if secret.is_a?(Proc) - - if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.action_controller.session = { :key => " + - "\"_myapp_session\", :secret => \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\" } " + - "in config/environment.rb" - end - - if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" - end - end - - def verifier_for(secret, digest) - key = secret.respond_to?(:call) ? secret.call : secret - ActiveSupport::MessageVerifier.new(key, digest) - end - - def generate_sid - ActiveSupport::SecureRandom.hex(16) - end - - def persistent_session_id!(data) - (data ||= {}).merge!(inject_persistent_session_id(data)) - end - - def inject_persistent_session_id(data) - requires_session_id?(data) ? { :session_id => generate_sid } : {} - end - - def requires_session_id?(data) - if data - data.respond_to?(:key?) && !data.key?(:session_id) - else - true - end - end - end - end -end diff --git a/actionpack/lib/action_controller/session/management.rb b/actionpack/lib/action_controller/session/management.rb index b556f04649..ffce8e1bd1 100644 --- a/actionpack/lib/action_controller/session/management.rb +++ b/actionpack/lib/action_controller/session/management.rb @@ -26,7 +26,7 @@ module ActionController #:nodoc: if defined? @@session_store @@session_store else - Session::CookieStore + ActionDispatch::Session::CookieStore end end diff --git a/actionpack/lib/action_controller/session/mem_cache_store.rb b/actionpack/lib/action_controller/session/mem_cache_store.rb deleted file mode 100644 index f745715a97..0000000000 --- a/actionpack/lib/action_controller/session/mem_cache_store.rb +++ /dev/null @@ -1,51 +0,0 @@ -begin - require_library_or_gem 'memcache' - - module ActionController - module Session - class MemCacheStore < AbstractStore - def initialize(app, options = {}) - # Support old :expires option - options[:expire_after] ||= options[:expires] - - super - - @default_options = { - :namespace => 'rack:session', - :memcache_server => 'localhost:11211' - }.merge(@default_options) - - @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options) - unless @pool.servers.any? { |s| s.alive? } - raise "#{self} unable to find server during initialization." - end - @mutex = Mutex.new - - super - end - - private - def get_session(env, sid) - sid ||= generate_sid - begin - session = @pool.get(sid) || {} - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - session = {} - end - [sid, session] - end - - def set_session(env, sid, session_data) - options = env['rack.session.options'] - expiry = options[:expire_after] || 0 - @pool.set(sid, session_data, expiry) - return true - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - return false - end - end - end - end -rescue LoadError - # MemCache wasn't available so neither can the store be -end diff --git a/actionpack/lib/action_controller/testing/assertions/response.rb b/actionpack/lib/action_controller/testing/assertions/response.rb index 5976090273..ca0a9bbf52 100644 --- a/actionpack/lib/action_controller/testing/assertions/response.rb +++ b/actionpack/lib/action_controller/testing/assertions/response.rb @@ -11,7 +11,7 @@ module ActionController # # You can also pass an explicit status number like assert_response(501) # or its symbolic equivalent assert_response(:not_implemented). - # See ActionController::StatusCodes for a full list. + # See ActionDispatch::StatusCodes for a full list. # # ==== Examples # @@ -27,7 +27,7 @@ module ActionController assert_block("") { true } # to count the assertion elsif type.is_a?(Fixnum) && @response.response_code == type assert_block("") { true } # to count the assertion - elsif type.is_a?(Symbol) && @response.response_code == ActionController::StatusCodes::SYMBOL_TO_STATUS_CODE[type] + elsif type.is_a?(Symbol) && @response.response_code == ActionDispatch::StatusCodes::SYMBOL_TO_STATUS_CODE[type] assert_block("") { true } # to count the assertion else if @response.error? diff --git a/actionpack/lib/action_controller/testing/integration.rb b/actionpack/lib/action_controller/testing/integration.rb index 163ba84a3e..0da23f9dc8 100644 --- a/actionpack/lib/action_controller/testing/integration.rb +++ b/actionpack/lib/action_controller/testing/integration.rb @@ -316,7 +316,7 @@ module ActionController @html_document = nil @status = status.to_i - @status_message = StatusCodes::STATUS_CODES[@status] + @status_message = ActionDispatch::StatusCodes::STATUS_CODES[@status] @headers = Rack::Utils::HeaderHash.new(headers) @@ -335,7 +335,7 @@ module ActionController else # Decorate responses from Rack Middleware and Rails Metal # as an Response for the purposes of integration testing - @response = Response.new + @response = ActionDispatch::Response.new @response.status = status.to_s @response.headers.replace(@headers) @response.body = @body @@ -374,7 +374,7 @@ module ActionController "SERVER_PORT" => https? ? "443" : "80", "HTTPS" => https? ? "on" : "off" } - UrlRewriter.new(Request.new(env), {}) + UrlRewriter.new(ActionDispatch::Request.new(env), {}) end def name_with_prefix(prefix, name) diff --git a/actionpack/lib/action_controller/testing/process.rb b/actionpack/lib/action_controller/testing/process.rb index 22b97fc157..199ffb702c 100644 --- a/actionpack/lib/action_controller/testing/process.rb +++ b/actionpack/lib/action_controller/testing/process.rb @@ -1,5 +1,5 @@ module ActionController #:nodoc: - class TestRequest < Request #:nodoc: + class TestRequest < ActionDispatch::Request #:nodoc: attr_accessor :cookies, :session_options attr_accessor :query_parameters, :path, :session attr_accessor :host @@ -270,7 +270,7 @@ module ActionController #:nodoc: # controller actions. # # See Response for more information on controller response objects. - class TestResponse < Response + class TestResponse < ActionDispatch::Response include TestResponseBehavior def recycle! diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb new file mode 100644 index 0000000000..393ccaa795 --- /dev/null +++ b/actionpack/lib/action_dispatch.rb @@ -0,0 +1,64 @@ +#-- +# Copyright (c) 2004-2009 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +begin + require 'active_support' +rescue LoadError + activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib" + if File.directory?(activesupport_path) + $:.unshift activesupport_path + require 'active_support' + end +end + +gem 'rack', '>= 0.9.0' +require 'rack' +require 'action_dispatch/rack' + +module ActionDispatch + autoload :Request, 'action_dispatch/http/request' + autoload :Response, 'action_dispatch/http/response' + autoload :StatusCodes, 'action_dispatch/http/status_codes' + + autoload :Failsafe, 'action_dispatch/middleware/failsafe' + autoload :ParamsParser, 'action_dispatch/middleware/params_parser' + autoload :RewindableInput, 'action_dispatch/middleware/rewindable_input' + + autoload :MiddlewareStack, 'action_dispatch/utils/middleware_stack' + autoload :UploadedFile, 'action_dispatch/utils/uploaded_file' + autoload :UploadedStringIO, 'action_dispatch/utils/uploaded_file' + autoload :UploadedTempfile, 'action_dispatch/utils/uploaded_file' + autoload :UrlEncodedPairParser, 'action_dispatch/utils/url_encoded_pair_parser' + + module Http + autoload :Headers, 'action_dispatch/http/headers' + end + + module Session + autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' + autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' + autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' + end +end + +autoload :Mime, 'action_dispatch/http/mime_type' diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb new file mode 100644 index 0000000000..2a41b4dbad --- /dev/null +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -0,0 +1,33 @@ +require 'active_support/memoizable' + +module ActionDispatch + module Http + class Headers < ::Hash + extend ActiveSupport::Memoizable + + def initialize(*args) + if args.size == 1 && args[0].is_a?(Hash) + super() + update(args[0]) + else + super + end + end + + def [](header_name) + if include?(header_name) + super + else + super(env_name(header_name)) + end + end + + private + # Converts a HTTP header name to an environment variable name. + def env_name(header_name) + "HTTP_#{header_name.upcase.gsub(/-/, '_')}" + end + memoize :env_name + end + end +end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb new file mode 100644 index 0000000000..0a7b1001c8 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -0,0 +1,214 @@ +require 'set' + +module Mime + SET = [] + EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + + def self.[](type) + Type.lookup_by_extension(type.to_s) + end + + # Encapsulates the notion of a mime type. Can be used at render time, for example, with: + # + # class PostsController < ActionController::Base + # def show + # @post = Post.find(params[:id]) + # + # 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 } + # end + # end + # end + class Type + @@html_types = Set.new [:html, :all] + cattr_reader :html_types + + # These are the content types which browsers can generate without using ajax, flash, etc + # i.e. following a link, getting an image or posting a form. CSRF protection + # only needs to protect against these types. + @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text] + cattr_reader :browser_generated_types + attr_reader :symbol + + @@unverifiable_types = Set.new [:text, :json, :csv, :xml, :rss, :atom, :yaml] + def self.unverifiable_types + ActiveSupport::Deprecation.warn("unverifiable_types is deprecated and has no effect", caller) + @@unverifiable_types + end + + # A simple helper class used in parsing the accept header + class AcceptItem #:nodoc: + attr_accessor :order, :name, :q + + def initialize(order, name, q=nil) + @order = order + @name = name.strip + q ||= 0.0 if @name == Mime::ALL # default wilcard 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 + end + + def ==(item) + name == (item.respond_to?(:name) ? item.name : item) + end + end + + class << self + def lookup(string) + LOOKUP[string] + end + + def lookup_by_extension(extension) + EXTENSION_LOOKUP[extension] + end + + # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for + # rendering different HTML versions depending on the user agent, like an iPhone. + def register_alias(string, symbol, extension_synonyms = []) + register(string, symbol, [], extension_synonyms, true) + end + + def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false) + Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) } + + SET << Mime.const_get(symbol.to_s.upcase) + + ([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup + ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last } + end + + def parse(accept_header) + if accept_header !~ /,/ + [Mime::Type.lookup(accept_header)] + else + # keep track of creation order to keep the subsequent sort stable + list = [] + accept_header.split(/,/).each_with_index do |header, index| + params, q = header.split(/;\s*q=/) + if params + params.strip! + list << AcceptItem.new(index, params, q) unless params.empty? + 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) + + 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 + end + idx += 1 + end + end + + list.map! { |i| Mime::Type.lookup(i.name) }.uniq! + list + end + end + end + + def initialize(string, symbol = nil, synonyms = []) + @symbol, @synonyms = symbol, synonyms + @string = string + end + + def to_s + @string + end + + def to_str + to_s + end + + def to_sym + @symbol || @string.to_sym + end + + def ===(list) + if list.is_a?(Array) + (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) } + else + super + end + end + + def ==(mime_type) + return false if mime_type.blank? + (@synonyms + [ self ]).any? do |synonym| + require "ruby-debug" + debugger if mime_type.is_a?(Array) + synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym + end + end + + def =~(mime_type) + return false if mime_type.blank? + regexp = Regexp.new(Regexp.quote(mime_type.to_s)) + (@synonyms + [ self ]).any? do |synonym| + synonym.to_s =~ regexp + end + end + + # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See + # ActionController::RequestForgeryProtection. + def verify_request? + @@browser_generated_types.include?(to_sym) + end + + def html? + @@html_types.include?(to_sym) || @string =~ /html/ + end + + private + def method_missing(method, *args) + if method.to_s =~ /(\w+)\?$/ + $1.downcase.to_sym == to_sym + else + super + end + end + end +end + +require 'action_dispatch/http/mime_types' diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb new file mode 100644 index 0000000000..2d7fba1173 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -0,0 +1,21 @@ +# Build list of Mime types for HTTP responses +# http://www.iana.org/assignments/media-types/ + +Mime::Type.register "*/*", :all +Mime::Type.register "text/plain", :text, [], %w(txt) +Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml ) +Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript ) +Mime::Type.register "text/css", :css +Mime::Type.register "text/calendar", :ics +Mime::Type.register "text/csv", :csv +Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) +Mime::Type.register "application/rss+xml", :rss +Mime::Type.register "application/atom+xml", :atom +Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ) + +Mime::Type.register "multipart/form-data", :multipart_form +Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form + +# http://www.ietf.org/rfc/rfc4627.txt +# http://www.json.org/JSONRequest.html +Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) \ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb new file mode 100755 index 0000000000..0da7daacf2 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -0,0 +1,492 @@ +require 'tempfile' +require 'stringio' +require 'strscan' + +require 'active_support/memoizable' +require 'action_controller/cgi_ext' + +module ActionDispatch + class Request < Rack::Request + extend ActiveSupport::Memoizable + + %w[ AUTH_TYPE GATEWAY_INTERFACE + PATH_TRANSLATED REMOTE_HOST + REMOTE_IDENT REMOTE_USER REMOTE_ADDR + SERVER_NAME SERVER_PROTOCOL + + HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING + HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env| + define_method(env.sub(/^HTTP_/n, '').downcase) do + @env[env] + end + end + + def key?(key) + @env.key?(key) + end + + HTTP_METHODS = %w(get head put post delete options) + HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } + + # Returns the true HTTP request \method as a lowercase symbol, such as + # :get. If the request \method is not listed in the HTTP_METHODS + # constant above, an UnknownHttpMethod exception is raised. + def request_method + HTTP_METHOD_LOOKUP[super] || raise(ActionController::UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") + end + memoize :request_method + + # Returns the HTTP request \method used for action processing as a + # lowercase symbol, such as :post. (Unlike #request_method, this + # method returns :get for a HEAD request because the two are + # functionally equivalent from the application's perspective.) + def method + request_method == :head ? :get : request_method + end + + # Is this a GET (or HEAD) request? Equivalent to request.method == :get. + def get? + method == :get + end + + # Is this a POST request? Equivalent to request.method == :post. + def post? + request_method == :post + end + + # Is this a PUT request? Equivalent to request.method == :put. + def put? + request_method == :put + end + + # Is this a DELETE request? Equivalent to request.method == :delete. + def delete? + request_method == :delete + end + + # Is this a HEAD request? Since request.method sees HEAD as :get, + # this \method checks the actual HTTP \method directly. + def head? + request_method == :head + end + + # Provides access to the request's HTTP headers, for example: + # + # request.headers["Content-Type"] # => "text/plain" + def headers + Http::Headers.new(@env) + end + memoize :headers + + # Returns the content length of the request as an integer. + def content_length + super.to_i + end + + # The MIME type of the HTTP request, such as Mime::XML. + # + # For backward compatibility, the post \format is extracted from the + # X-Post-Data-Format HTTP header if present. + def content_type + if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + Mime::Type.lookup($1.strip.downcase) + else + nil + end + end + memoize :content_type + + # Returns the accepted MIME type for the request. + def accepts + header = @env['HTTP_ACCEPT'].to_s.strip + + fallback = xhr? ? Mime::JS : Mime::HTML + + if header.empty? + [content_type, fallback, Mime::ALL].compact + else + ret = Mime::Type.parse(header) + if ret.last == Mime::ALL + ret.insert(-2, fallback) + end + ret + end + end + memoize :accepts + + def if_modified_since + if since = env['HTTP_IF_MODIFIED_SINCE'] + Time.rfc2822(since) rescue nil + end + end + memoize :if_modified_since + + def if_none_match + env['HTTP_IF_NONE_MATCH'] + 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 + end + + # Check response freshness (Last-Modified and ETag) against request + # If-Modified-Since and If-None-Match conditions. If both headers are + # supplied, both must match, or the request is not considered fresh. + def fresh?(response) + case + when if_modified_since && if_none_match + not_modified?(response.last_modified) && etag_matches?(response.etag) + when if_modified_since + not_modified?(response.last_modified) + when if_none_match + etag_matches?(response.etag) + else + false + end + end + + ONLY_ALL = [Mime::ALL].freeze + + # Returns the Mime type for the \format used in the request. + # + # GET /posts/5.xml | request.format => Mime::XML + # GET /posts/5.xhtml | request.format => Mime::HTML + # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of ActionController::Base.use_accept_header + + def format(view_path = []) + @format ||= + if parameters[:format] + Mime[parameters[:format]] + elsif ActionController::Base.use_accept_header && !(accepts == ONLY_ALL) + accepts.first + elsif xhr? then Mime::JS + else Mime::HTML + end + end + + def formats + @formats = + if ActionController::Base.use_accept_header + ret = Array(Mime[parameters[:format]] || accepts) + else + [format] + end + end + + # Sets the \format by string extension, which can be used to force custom formats + # that are not controlled by the extension. + # + # class ApplicationController < ActionController::Base + # before_filter :adjust_format_for_iphone + # + # private + # def adjust_format_for_iphone + # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] + # end + # end + def format=(extension) + parameters[:format] = extension.to_s + @format = Mime::Type.lookup_by_extension(parameters[:format]) + end + + # Returns a symbolized version of the :format parameter of the request. + # If no \format is given it returns :jsfor Ajax requests and :html + # otherwise. + def template_format + parameter_format = parameters[:format] + + if parameter_format + parameter_format + elsif xhr? + :js + else + :html + end + end + + def cache_format + parameters[:format] + end + + # Returns true if the request's "X-Requested-With" header contains + # "XMLHttpRequest". (The Prototype Javascript library sends this header with + # every Ajax request.) + def xml_http_request? + !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i) + end + alias xhr? :xml_http_request? + + # Which IP addresses are "trusted proxies" that can be stripped from + # the right-hand-side of X-Forwarded-For + TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i + + # Determines originating IP address. REMOTE_ADDR is the standard + # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or + # HTTP_X_FORWARDED_FOR are set by proxies so check for these if + # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- + # delimited list in the case of multiple chained proxies; the last + # address which is not trusted is the originating IP. + def remote_ip + remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/) + + unless remote_addr_list.blank? + not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES} + return not_trusted_addrs.first unless not_trusted_addrs.empty? + end + remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',') + + if @env.include? 'HTTP_CLIENT_IP' + if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP']) + # We don't know which came from the proxy, and which from the user + raise ActionController::ActionControllerError.new(< 1 && TRUSTED_PROXIES =~ remote_ips.last.strip + remote_ips.pop + end + + return remote_ips.last.strip + end + + @env['REMOTE_ADDR'] + end + memoize :remote_ip + + # Returns the lowercase name of the HTTP server software. + def server_software + (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil + end + memoize :server_software + + # Returns the complete URL used for this request. + def url + protocol + host_with_port + request_uri + end + memoize :url + + # Returns 'https://' if this is an SSL request and 'http://' otherwise. + def protocol + ssl? ? 'https://' : 'http://' + end + memoize :protocol + + # Is this an SSL request? + def ssl? + @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' + end + + # Returns the \host for this request, such as "example.com". + def raw_host_with_port + if forwarded = env["HTTP_X_FORWARDED_HOST"] + forwarded.split(/,\s?/).last + else + env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + end + end + + # Returns the host for this request, such as example.com. + def host + raw_host_with_port.sub(/:\d+$/, '') + end + memoize :host + + # Returns a \host:\port string for this request, such as "example.com" or + # "example.com:8080". + def host_with_port + "#{host}#{port_string}" + end + memoize :host_with_port + + # Returns the port number of this request as an integer. + def port + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end + end + memoize :port + + # Returns the standard \port number for this request's protocol. + def standard_port + case protocol + when 'https://' then 443 + else 80 + end + end + + # Returns a \port suffix like ":8080" if the \port number of this request + # is not the default HTTP \port 80 or HTTPS \port 443. + def port_string + port == standard_port ? '' : ":#{port}" + end + + # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify + # a different tld_length, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". + def domain(tld_length = 1) + return nil unless named_host?(host) + + host.split('.').last(1 + tld_length).join('.') + end + + # Returns all the \subdomains as an array, so ["dev", "www"] would be + # returned for "dev.www.rubyonrails.org". You can specify a different tld_length, + # such as 2 to catch ["www"] instead of ["www", "rubyonrails"] + # in "www.rubyonrails.co.uk". + def subdomains(tld_length = 1) + return [] unless named_host?(host) + parts = host.split('.') + parts[0..-(tld_length+2)] + end + + # Returns the query string, accounting for server idiosyncrasies. + def query_string + @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') + end + memoize :query_string + + # Returns the request URI, accounting for server idiosyncrasies. + # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. + def request_uri + if uri = @env['REQUEST_URI'] + # Remove domain, which webrick puts into the request_uri. + (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri + else + # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. + uri = @env['PATH_INFO'].to_s + + if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) + uri = uri.sub(/#{script_filename}\//, '') + end + + env_qs = @env['QUERY_STRING'].to_s + uri += "?#{env_qs}" unless env_qs.empty? + + if uri.blank? + @env.delete('REQUEST_URI') + else + @env['REQUEST_URI'] = uri + end + end + end + memoize :request_uri + + # Returns the interpreted \path to requested resource after all the installation + # directory of this application was taken into account. + def path + path = request_uri.to_s[/\A[^\?]*/] + path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') + path + end + memoize :path + + # Read the request \body. This is useful for web services that need to + # work with raw requests directly. + def raw_post + unless @env.include? 'RAW_POST_DATA' + @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i) + body.rewind if body.respond_to?(:rewind) + end + @env['RAW_POST_DATA'] + end + + # Returns both GET and POST \parameters in a single hash. + def parameters + @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access + end + alias_method :params, :parameters + + def path_parameters=(parameters) #:nodoc: + @env["rack.routing_args"] = parameters + @symbolized_path_parameters = @parameters = nil + end + + # The same as path_parameters with explicitly symbolized keys. + def symbolized_path_parameters + @symbolized_path_parameters ||= path_parameters.symbolize_keys + end + + # Returns a hash with the \parameters used to form the \path of the request. + # Returned hash keys are strings: + # + # {'action' => 'my_action', 'controller' => 'my_controller'} + # + # See symbolized_path_parameters for symbolized keys. + def path_parameters + @env["rack.routing_args"] ||= {} + end + + # The request body is an IO input stream. If the RAW_POST_DATA environment + # variable is already set, wrap it in a StringIO. + def body + if raw_post = @env['RAW_POST_DATA'] + raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) + StringIO.new(raw_post) + else + @env['rack.input'] + end + end + + def form_data? + FORM_DATA_MEDIA_TYPES.include?(content_type.to_s) + end + + # Override Rack's GET method to support nested query strings + def GET + @env["action_controller.request.query_parameters"] ||= UrlEncodedPairParser.parse_query_parameters(query_string) + end + alias_method :query_parameters, :GET + + # Override Rack's POST method to support nested query strings + def POST + @env["action_controller.request.request_parameters"] ||= UrlEncodedPairParser.parse_hash_parameters(super) + end + alias_method :request_parameters, :POST + + def body_stream #:nodoc: + @env['rack.input'] + end + + def session + @env['rack.session'] ||= {} + end + + def session=(session) #:nodoc: + @env['rack.session'] = session + end + + def reset_session + @env['rack.session'] = {} + end + + def session_options + @env['rack.session.options'] ||= {} + end + + def session_options=(options) + @env['rack.session.options'] = options + end + + def server_port + @env['SERVER_PORT'].to_i + end + + private + def named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + end + end +end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb new file mode 100644 index 0000000000..e1d8ee3527 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -0,0 +1,255 @@ +require 'digest/md5' + +module ActionDispatch # :nodoc: + # Represents an HTTP response generated by a controller action. One can use + # an ActionController::Response object to retrieve the current state + # of the response, or customize the response. An Response object can + # either represent a "real" HTTP response (i.e. one that is meant to be sent + # back to the web browser) or a test response (i.e. one that is generated + # from integration tests). See CgiResponse and TestResponse, respectively. + # + # Response is mostly a Ruby on Rails framework implement detail, and + # should never be used directly in controllers. Controllers should use the + # methods defined in ActionController::Base instead. For example, if you want + # to set the HTTP response's content MIME type, then use + # ActionControllerBase#headers instead of Response#headers. + # + # Nevertheless, integration tests may want to inspect controller responses in + # more detail, and that's when Response can be useful for application + # developers. Integration test methods such as + # ActionController::Integration::Session#get and + # ActionController::Integration::Session#post return objects of type + # TestResponse (which are of course also of type Response). + # + # For example, the following demo integration "test" prints the body of the + # controller response to the console: + # + # class DemoControllerTest < ActionController::IntegrationTest + # def test_print_root_path_to_console + # get('/') + # puts @response.body + # end + # end + class Response < Rack::Response + DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } + attr_accessor :request + + attr_accessor :session, :assigns, :template, :layout + attr_accessor :redirected_to, :redirected_to_method_params + + delegate :default_charset, :to => 'ActionController::Base' + + def initialize + @status = 200 + @header = DEFAULT_HEADERS.dup + + @writer = lambda { |x| @body << x } + @block = nil + + @body = "", + @session, @assigns = [], [] + end + + def location; headers['Location'] end + def location=(url) headers['Location'] = url end + + + # Sets the HTTP response's content MIME type. For example, in the controller + # you could write this: + # + # response.content_type = "text/plain" + # + # 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. + def content_type=(mime_type) + self.headers["Content-Type"] = + if mime_type =~ /charset/ || (c = charset).nil? + mime_type.to_s + else + "#{mime_type}; charset=#{c}" + end + end + + # Returns the response's content MIME type, or nil if content type has been set. + def content_type + content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0] + content_type.blank? ? nil : content_type + end + + # Set the charset of the Content-Type header. Set to nil to remove it. + # If no content type is set, it defaults to HTML. + def charset=(charset) + headers["Content-Type"] = + if charset + "#{content_type || Mime::HTML}; charset=#{charset}" + else + content_type || Mime::HTML.to_s + end + end + + def charset + charset = String(headers["Content-Type"] || headers["type"]).split(";")[1] + charset.blank? ? nil : charset.strip.split("=")[1] + end + + def last_modified + if last = headers['Last-Modified'] + Time.httpdate(last) + end + end + + def last_modified? + headers.include?('Last-Modified') + end + + def last_modified=(utc_time) + headers['Last-Modified'] = utc_time.httpdate + end + + def etag + headers['ETag'] + end + + def etag? + headers.include?('ETag') + end + + def etag=(etag) + if etag.blank? + headers.delete('ETag') + else + headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}") + end + end + + def redirect(url, status) + self.status = status + self.location = url.gsub(/[\r\n]/, '') + self.body = "You are being redirected." + end + + def sending_file? + headers["Content-Transfer-Encoding"] == "binary" + end + + def assign_default_content_type_and_charset! + self.content_type ||= Mime::HTML + self.charset ||= default_charset unless sending_file? + end + + def prepare! + assign_default_content_type_and_charset! + handle_conditional_get! + set_content_length! + convert_content_type! + convert_language! + convert_expires! + convert_cookies! + end + + def each(&callback) + if @body.respond_to?(:call) + @writer = lambda { |x| callback.call(x) } + @body.call(self, self) + elsif @body.is_a?(String) + @body.each_line(&callback) + else + @body.each(&callback) + end + + @writer = callback + @block.call(self) if @block + end + + def write(str) + @writer.call str.to_s + str + end + + # Over Rack::Response#set_cookie to add HttpOnly option + def set_cookie(key, value) + case value + when Hash + domain = "; domain=" + value[:domain] if value[:domain] + path = "; path=" + value[:path] if value[:path] + # According to RFC 2109, we need dashes here. + # N.B.: cgi.rb uses spaces... + expires = "; expires=" + value[:expires].clone.gmtime. + strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] + secure = "; secure" if value[:secure] + httponly = "; HttpOnly" if value[:http_only] + value = value[:value] + end + value = [value] unless Array === value + cookie = ::Rack::Utils.escape(key) + "=" + + value.map { |v| ::Rack::Utils.escape v }.join("&") + + "#{domain}#{path}#{expires}#{secure}#{httponly}" + + case self["Set-Cookie"] + when Array + self["Set-Cookie"] << cookie + when String + self["Set-Cookie"] = [self["Set-Cookie"], cookie] + when nil + self["Set-Cookie"] = cookie + end + end + + private + def handle_conditional_get! + if etag? || last_modified? + set_conditional_cache_control! + elsif nonempty_ok_response? + self.etag = body + + if request && request.etag_matches?(etag) + self.status = '304 Not Modified' + self.body = '' + end + + set_conditional_cache_control! + end + end + + def nonempty_ok_response? + ok = !status || status.to_s[0..2] == '200' + ok && body.is_a?(String) && !body.empty? + end + + def set_conditional_cache_control! + if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control'] + headers['Cache-Control'] = 'private, max-age=0, must-revalidate' + end + end + + def convert_content_type! + headers['Content-Type'] ||= "text/html" + headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset'] + end + + # Don't set the Content-Length for block-based bodies as that would mean + # reading it all into memory. Not nice for, say, a 2GB streaming file. + def set_content_length! + if status && status.to_s[0..2] == '204' + headers.delete('Content-Length') + elsif length = headers['Content-Length'] + headers['Content-Length'] = length.to_s + elsif !body.respond_to?(:call) && (!status || status.to_s[0..2] != '304') + headers["Content-Length"] = body.size.to_s + end + end + + def convert_language! + headers["Content-Language"] = headers.delete("language") if headers["language"] + end + + def convert_expires! + headers["Expires"] = headers.delete("") if headers["expires"] + end + + def convert_cookies! + headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact + end + end +end diff --git a/actionpack/lib/action_dispatch/http/status_codes.rb b/actionpack/lib/action_dispatch/http/status_codes.rb new file mode 100644 index 0000000000..cec9d2e3a1 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/status_codes.rb @@ -0,0 +1,88 @@ +module ActionDispatch + module StatusCodes #:nodoc: + # Defines the standard HTTP status codes, by integer, with their + # corresponding default message texts. + # Source: http://www.iana.org/assignments/http-status-codes + STATUS_CODES = { + 100 => "Continue", + 101 => "Switching Protocols", + 102 => "Processing", + + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 203 => "Non-Authoritative Information", + 204 => "No Content", + 205 => "Reset Content", + 206 => "Partial Content", + 207 => "Multi-Status", + 226 => "IM Used", + + 300 => "Multiple Choices", + 301 => "Moved Permanently", + 302 => "Found", + 303 => "See Other", + 304 => "Not Modified", + 305 => "Use Proxy", + 307 => "Temporary Redirect", + + 400 => "Bad Request", + 401 => "Unauthorized", + 402 => "Payment Required", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 406 => "Not Acceptable", + 407 => "Proxy Authentication Required", + 408 => "Request Timeout", + 409 => "Conflict", + 410 => "Gone", + 411 => "Length Required", + 412 => "Precondition Failed", + 413 => "Request Entity Too Large", + 414 => "Request-URI Too Long", + 415 => "Unsupported Media Type", + 416 => "Requested Range Not Satisfiable", + 417 => "Expectation Failed", + 422 => "Unprocessable Entity", + 423 => "Locked", + 424 => "Failed Dependency", + 426 => "Upgrade Required", + + 500 => "Internal Server Error", + 501 => "Not Implemented", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Timeout", + 505 => "HTTP Version Not Supported", + 507 => "Insufficient Storage", + 510 => "Not Extended" + } + + # Provides a symbol-to-fixnum lookup for converting a symbol (like + # :created or :not_implemented) into its corresponding HTTP status + # code (like 200 or 501). + SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)| + hash[message.gsub(/ /, "").underscore.to_sym] = code + hash + end + + # Given a status parameter, determine whether it needs to be converted + # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup + # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE + # hash to convert it. + def interpret_status(status) + case status + when Fixnum then + "#{status} #{STATUS_CODES[status]}".strip + when Symbol then + interpret_status(SYMBOL_TO_STATUS_CODE[status] || + "500 Unknown Status #{status.inspect}") + else + status.to_s + end + end + private :interpret_status + + end +end \ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/failsafe.rb b/actionpack/lib/action_dispatch/middleware/failsafe.rb new file mode 100644 index 0000000000..7379a696aa --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/failsafe.rb @@ -0,0 +1,52 @@ +module ActionDispatch + class Failsafe + cattr_accessor :error_file_path + self.error_file_path = Rails.public_path if defined?(Rails.public_path) + + def initialize(app) + @app = app + end + + def call(env) + @app.call(env) + rescue Exception => exception + # Reraise exception in test environment + if env["rack.test"] + raise exception + else + failsafe_response(exception) + end + end + + private + def failsafe_response(exception) + log_failsafe_exception(exception) + [500, {'Content-Type' => 'text/html'}, failsafe_response_body] + rescue Exception => failsafe_error # Logger or IO errors + $stderr.puts "Error during failsafe response: #{failsafe_error}" + end + + def failsafe_response_body + error_path = "#{self.class.error_file_path}/500.html" + if File.exist?(error_path) + File.read(error_path) + else + "

500 Internal Server Error

" + end + end + + def log_failsafe_exception(exception) + message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: 500 Internal Server Error\n" + message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception + failsafe_logger.fatal(message) + end + + def failsafe_logger + if defined?(Rails) && Rails.logger + Rails.logger + else + Logger.new($stderr) + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb new file mode 100644 index 0000000000..6df572268c --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -0,0 +1,71 @@ +module ActionDispatch + class ParamsParser + ActionController::Base.param_parsers[Mime::XML] = :xml_simple + ActionController::Base.param_parsers[Mime::JSON] = :json + + def initialize(app) + @app = app + end + + def call(env) + if params = parse_formatted_parameters(env) + env["action_controller.request.request_parameters"] = params + end + + @app.call(env) + end + + private + def parse_formatted_parameters(env) + request = Request.new(env) + + return false if request.content_length.zero? + + mime_type = content_type_from_legacy_post_data_format_header(env) || request.content_type + strategy = ActionController::Base.param_parsers[mime_type] + + return false unless strategy + + case strategy + when Proc + strategy.call(request.raw_post) + when :xml_simple, :xml_node + body = request.raw_post + body.blank? ? {} : Hash.from_xml(body).with_indifferent_access + when :yaml + YAML.load(request.raw_post) + when :json + body = request.raw_post + if body.blank? + {} + else + data = ActiveSupport::JSON.decode(body) + data = {:_json => data} unless data.is_a?(Hash) + data.with_indifferent_access + end + else + false + end + rescue Exception => e # YAML, XML or Ruby code block errors + raise + { "body" => request.raw_post, + "content_type" => request.content_type, + "content_length" => request.content_length, + "exception" => "#{e.message} (#{e.class})", + "backtrace" => e.backtrace } + end + + def content_type_from_legacy_post_data_format_header(env) + if x_post_format = env['HTTP_X_POST_DATA_FORMAT'] + case x_post_format.to_s.downcase + when 'yaml' + return Mime::YAML + when 'xml' + return Mime::XML + end + end + + nil + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/rewindable_input.rb b/actionpack/lib/action_dispatch/middleware/rewindable_input.rb new file mode 100644 index 0000000000..ac2194eead --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/rewindable_input.rb @@ -0,0 +1,28 @@ +module ActionDispatch + class RewindableInput + class RewindableIO < ActiveSupport::BasicObject + def initialize(io) + @io = io + @rewindable = io.is_a?(StringIO) + end + + def method_missing(method, *args, &block) + unless @rewindable + @io = StringIO.new(@io.read) + @rewindable = true + end + + @io.__send__(method, *args, &block) + end + end + + def initialize(app) + @app = app + end + + def call(env) + env['rack.input'] = RewindableIO.new(env['rack.input']) + @app.call(env) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb new file mode 100644 index 0000000000..e745997dda --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -0,0 +1,166 @@ +require 'rack/utils' + +module ActionDispatch + module Session + class AbstractStore + ENV_SESSION_KEY = 'rack.session'.freeze + ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze + + HTTP_COOKIE = 'HTTP_COOKIE'.freeze + SET_COOKIE = 'Set-Cookie'.freeze + + class SessionHash < Hash + def initialize(by, env) + super() + @by = by + @env = env + @loaded = false + end + + def id + load! unless @loaded + @id + end + + def session_id + ActiveSupport::Deprecation.warn( + "ActionController::Session::AbstractStore::SessionHash#session_id" + + "has been deprecated.Please use #id instead.", caller) + id + end + + def [](key) + load! unless @loaded + super + end + + def []=(key, value) + load! unless @loaded + super + end + + def to_hash + h = {}.replace(self) + h.delete_if { |k,v| v.nil? } + h + end + + def data + ActiveSupport::Deprecation.warn( + "ActionController::Session::AbstractStore::SessionHash#data" + + "has been deprecated.Please use #to_hash instead.", caller) + to_hash + end + + private + def loaded? + @loaded + end + + def load! + @id, session = @by.send(:load_session, @env) + replace(session) + @loaded = true + end + end + + DEFAULT_OPTIONS = { + :key => '_session_id', + :path => '/', + :domain => nil, + :expire_after => nil, + :secure => false, + :httponly => true, + :cookie_only => true + } + + def initialize(app, options = {}) + # Process legacy CGI options + options = options.symbolize_keys + if options.has_key?(:session_path) + options[:path] = options.delete(:session_path) + end + if options.has_key?(:session_key) + options[:key] = options.delete(:session_key) + end + if options.has_key?(:session_http_only) + options[:httponly] = options.delete(:session_http_only) + end + + @app = app + @default_options = DEFAULT_OPTIONS.merge(options) + @key = @default_options[:key] + @cookie_only = @default_options[:cookie_only] + end + + def call(env) + session = SessionHash.new(self, env) + + env[ENV_SESSION_KEY] = session + env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup + + response = @app.call(env) + + session_data = env[ENV_SESSION_KEY] + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) + options = env[ENV_SESSION_OPTIONS_KEY] + + if session_data.is_a?(AbstractStore::SessionHash) + sid = session_data.id + else + sid = generate_sid + end + + unless set_session(env, sid, session_data.to_hash) + return response + end + + cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid) + cookie << "; domain=#{options[:domain]}" if options[:domain] + cookie << "; path=#{options[:path]}" if options[:path] + if options[:expire_after] + expiry = Time.now + options[:expire_after] + cookie << "; expires=#{expiry.httpdate}" + end + cookie << "; Secure" if options[:secure] + cookie << "; HttpOnly" if options[:httponly] + + headers = response[1] + case a = headers[SET_COOKIE] + when Array + a << cookie + when String + headers[SET_COOKIE] = [a, cookie] + when nil + headers[SET_COOKIE] = cookie + end + end + + response + end + + private + def generate_sid + ActiveSupport::SecureRandom.hex(16) + end + + def load_session(env) + request = Rack::Request.new(env) + sid = request.cookies[@key] + unless @cookie_only + sid ||= request.params[@key] + end + sid, session = get_session(env, sid) + [sid, session] + end + + def get_session(env, sid) + raise '#get_session needs to be implemented.' + end + + def set_session(env, sid, session_data) + raise '#set_session needs to be implemented.' + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb new file mode 100644 index 0000000000..293fbca7cf --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -0,0 +1,222 @@ +module ActionDispatch + module Session + # This cookie-based session store is the Rails default. Sessions typically + # contain at most a user_id and flash message; both fit within the 4K cookie + # size limit. Cookie-based sessions are dramatically faster than the + # alternatives. + # + # If you have more than 4K of session data or don't want your data to be + # visible to the user, pick another session store. + # + # CookieOverflow is raised if you attempt to store more than 4K of data. + # + # A message digest is included with the cookie to ensure data integrity: + # a user cannot alter his +user_id+ without knowing the secret key + # included in the hash. New apps are generated with a pregenerated secret + # in config/environment.rb. Set your own for old apps you're upgrading. + # + # Session options: + # + # * :secret: An application-wide key string or block returning a + # string called per generated digest. The block is called with the + # 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: + # + # :secret => '449fe2e7daee471bffae2fd8dc02313d' + # :secret => Proc.new { User.current_user.secret_key } + # + # * :digest: The message digest algorithm used to verify session + # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL, + # such as 'MD5', 'RIPEMD160', 'SHA256', etc. + # + # To generate a secret key for an existing application, run + # "rake secret" and set the key in config/environment.rb. + # + # Note that changing digest or secret invalidates all existing sessions! + class CookieStore + # Cookies can typically store 4096 bytes. + MAX = 4096 + SECRET_MIN_LENGTH = 30 # characters + + DEFAULT_OPTIONS = { + :key => '_session_id', + :domain => nil, + :path => "/", + :expire_after => nil, + :httponly => true + }.freeze + + ENV_SESSION_KEY = "rack.session".freeze + ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze + HTTP_SET_COOKIE = "Set-Cookie".freeze + + # Raised when storing more than 4K of session data. + class CookieOverflow < StandardError; end + + def initialize(app, options = {}) + # Process legacy CGI options + options = options.symbolize_keys + if options.has_key?(:session_path) + options[:path] = options.delete(:session_path) + end + if options.has_key?(:session_key) + options[:key] = options.delete(:session_key) + end + if options.has_key?(:session_http_only) + options[:httponly] = options.delete(:session_http_only) + end + + @app = app + + # The session_key option is required. + ensure_session_key(options[:key]) + @key = options.delete(:key).freeze + + # The secret option is required. + ensure_secret_secure(options[:secret]) + @secret = options.delete(:secret).freeze + + @digest = options.delete(:digest) || 'SHA1' + @verifier = verifier_for(@secret, @digest) + + @default_options = DEFAULT_OPTIONS.merge(options).freeze + + freeze + end + + def call(env) + env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env) + env[ENV_SESSION_OPTIONS_KEY] = @default_options + + status, headers, body = @app.call(env) + + session_data = env[ENV_SESSION_KEY] + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) + session_data = marshal(session_data.to_hash) + + raise CookieOverflow if session_data.size > MAX + + options = env[ENV_SESSION_OPTIONS_KEY] + cookie = Hash.new + cookie[:value] = session_data + unless options[:expire_after].nil? + cookie[:expires] = Time.now + options[:expire_after] + end + + cookie = build_cookie(@key, cookie.merge(options)) + case headers[HTTP_SET_COOKIE] + when Array + headers[HTTP_SET_COOKIE] << cookie + when String + headers[HTTP_SET_COOKIE] = [headers[HTTP_SET_COOKIE], cookie] + when nil + headers[HTTP_SET_COOKIE] = cookie + end + end + + [status, headers, body] + end + + private + # Should be in Rack::Utils soon + def build_cookie(key, value) + case value + when Hash + domain = "; domain=" + value[:domain] if value[:domain] + path = "; path=" + value[:path] if value[:path] + # According to RFC 2109, we need dashes here. + # N.B.: cgi.rb uses spaces... + expires = "; expires=" + value[:expires].clone.gmtime. + strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] + secure = "; secure" if value[:secure] + httponly = "; httponly" if value[:httponly] + value = value[:value] + end + value = [value] unless Array === value + cookie = Rack::Utils.escape(key) + "=" + + value.map { |v| Rack::Utils.escape(v) }.join("&") + + "#{domain}#{path}#{expires}#{secure}#{httponly}" + end + + def load_session(env) + request = Rack::Request.new(env) + session_data = request.cookies[@key] + data = unmarshal(session_data) || persistent_session_id!({}) + [data[:session_id], data] + end + + # Marshal a session hash into safe cookie data. Include an integrity hash. + def marshal(session) + @verifier.generate(persistent_session_id!(session)) + end + + # Unmarshal cookie data to a hash and verify its integrity. + def unmarshal(cookie) + persistent_session_id!(@verifier.verify(cookie)) if cookie + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end + + def ensure_session_key(key) + if key.blank? + raise ArgumentError, 'A key is required to write a ' + + 'cookie containing the session data. Use ' + + 'config.action_controller.session = { :key => ' + + '"_myapp_session", :secret => "some secret phrase" } in ' + + 'config/environment.rb' + end + end + + # To prevent users from using something insecure like "Password" we make sure that the + # secret they've provided is at least 30 characters in length. + def ensure_secret_secure(secret) + # There's no way we can do this check if they've provided a proc for the + # secret. + return true if secret.is_a?(Proc) + + if secret.blank? + raise ArgumentError, "A secret is required to generate an " + + "integrity hash for cookie session data. Use " + + "config.action_controller.session = { :key => " + + "\"_myapp_session\", :secret => \"some secret phrase of at " + + "least #{SECRET_MIN_LENGTH} characters\" } " + + "in config/environment.rb" + end + + if secret.length < SECRET_MIN_LENGTH + raise ArgumentError, "Secret should be something secure, " + + "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " + + "provided, \"#{secret}\", is shorter than the minimum length " + + "of #{SECRET_MIN_LENGTH} characters" + end + end + + def verifier_for(secret, digest) + key = secret.respond_to?(:call) ? secret.call : secret + ActiveSupport::MessageVerifier.new(key, digest) + end + + def generate_sid + ActiveSupport::SecureRandom.hex(16) + end + + def persistent_session_id!(data) + (data ||= {}).merge!(inject_persistent_session_id(data)) + end + + def inject_persistent_session_id(data) + requires_session_id?(data) ? { :session_id => generate_sid } : {} + end + + def requires_session_id?(data) + if data + data.respond_to?(:key?) && !data.key?(:session_id) + else + true + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb new file mode 100644 index 0000000000..8f448970d9 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -0,0 +1,51 @@ +begin + require_library_or_gem 'memcache' + + module ActionDispatch + module Session + class MemCacheStore < AbstractStore + def initialize(app, options = {}) + # Support old :expires option + options[:expire_after] ||= options[:expires] + + super + + @default_options = { + :namespace => 'rack:session', + :memcache_server => 'localhost:11211' + }.merge(@default_options) + + @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options) + unless @pool.servers.any? { |s| s.alive? } + raise "#{self} unable to find server during initialization." + end + @mutex = Mutex.new + + super + end + + private + def get_session(env, sid) + sid ||= generate_sid + begin + session = @pool.get(sid) || {} + rescue MemCache::MemCacheError, Errno::ECONNREFUSED + session = {} + end + [sid, session] + end + + def set_session(env, sid, session_data) + options = env['rack.session.options'] + expiry = options[:expire_after] || 0 + @pool.set(sid, session_data, expiry) + return true + rescue MemCache::MemCacheError, Errno::ECONNREFUSED + return false + end + end + end + end +rescue LoadError + # MemCache wasn't available so neither can the store be +end diff --git a/actionpack/lib/action_dispatch/rack.rb b/actionpack/lib/action_dispatch/rack.rb new file mode 100644 index 0000000000..69df9dac06 --- /dev/null +++ b/actionpack/lib/action_dispatch/rack.rb @@ -0,0 +1,3 @@ +require 'action_dispatch/rack/lock' +require 'action_dispatch/rack/multipart' +require 'action_dispatch/rack/parse_query' diff --git a/actionpack/lib/action_dispatch/rack/lock.rb b/actionpack/lib/action_dispatch/rack/lock.rb new file mode 100644 index 0000000000..9bf1889065 --- /dev/null +++ b/actionpack/lib/action_dispatch/rack/lock.rb @@ -0,0 +1,21 @@ +module Rack + # Rack::Lock was commited to Rack core + # http://github.com/rack/rack/commit/7409b0c + # Remove this when Rack 1.0 is released + unless defined? Lock + class Lock + FLAG = 'rack.multithread'.freeze + + def initialize(app, lock = Mutex.new) + @app, @lock = app, lock + end + + def call(env) + old, env[FLAG] = env[FLAG], false + @lock.synchronize { @app.call(env) } + ensure + env[FLAG] = old + end + end + end +end diff --git a/actionpack/lib/action_dispatch/rack/multipart.rb b/actionpack/lib/action_dispatch/rack/multipart.rb new file mode 100644 index 0000000000..3b142307e9 --- /dev/null +++ b/actionpack/lib/action_dispatch/rack/multipart.rb @@ -0,0 +1,22 @@ +module Rack + module Utils + module Multipart + class << self + def parse_multipart_with_rewind(env) + result = parse_multipart_without_rewind(env) + + begin + env['rack.input'].rewind if env['rack.input'].respond_to?(:rewind) + rescue Errno::ESPIPE + # Handles exceptions raised by input streams that cannot be rewound + # such as when using plain CGI under Apache + end + + result + end + + alias_method_chain :parse_multipart, :rewind + end + end + end +end diff --git a/actionpack/lib/action_dispatch/rack/parse_query.rb b/actionpack/lib/action_dispatch/rack/parse_query.rb new file mode 100644 index 0000000000..2f21a57770 --- /dev/null +++ b/actionpack/lib/action_dispatch/rack/parse_query.rb @@ -0,0 +1,18 @@ +# Rack does not automatically cleanup Safari 2 AJAX POST body +# This has not yet been commited to Rack, please +1 this ticket: +# http://rack.lighthouseapp.com/projects/22435/tickets/19 + +module Rack + module Utils + alias_method :parse_query_without_ajax_body_cleanup, :parse_query + module_function :parse_query_without_ajax_body_cleanup + + def parse_query(qs, d = '&;') + qs = qs.dup + qs.chop! if qs[-1] == 0 + qs.gsub!(/&_=$/, '') + parse_query_without_ajax_body_cleanup(qs, d) + end + module_function :parse_query + end +end diff --git a/actionpack/lib/action_dispatch/utils/middleware_stack.rb b/actionpack/lib/action_dispatch/utils/middleware_stack.rb new file mode 100644 index 0000000000..924e3dbbc2 --- /dev/null +++ b/actionpack/lib/action_dispatch/utils/middleware_stack.rb @@ -0,0 +1,109 @@ +module ActionDispatch + class MiddlewareStack < Array + class Middleware + def self.new(klass, *args, &block) + if klass.is_a?(self) + klass + else + super + end + end + + attr_reader :args, :block + + def initialize(klass, *args, &block) + @klass = klass + + options = args.extract_options! + if options.has_key?(:if) + @conditional = options.delete(:if) + else + @conditional = true + end + args << options unless options.empty? + + @args = args + @block = block + end + + def klass + if @klass.is_a?(Class) + @klass + else + @klass.to_s.constantize + end + rescue NameError + @klass + end + + def active? + if @conditional.respond_to?(:call) + @conditional.call + else + @conditional + end + end + + def ==(middleware) + case middleware + when Middleware + klass == middleware.klass + when Class + klass == middleware + else + klass == middleware.to_s.constantize + end + end + + def inspect + str = klass.to_s + args.each { |arg| str += ", #{arg.inspect}" } + str + end + + def build(app) + if block + klass.new(app, *args, &block) + else + klass.new(app, *args) + end + end + end + + def initialize(*args, &block) + super(*args) + block.call(self) if block_given? + end + + def insert(index, *args, &block) + index = self.index(index) unless index.is_a?(Integer) + middleware = Middleware.new(*args, &block) + super(index, middleware) + end + + alias_method :insert_before, :insert + + def insert_after(index, *args, &block) + index = self.index(index) unless index.is_a?(Integer) + insert(index + 1, *args, &block) + end + + def swap(target, *args, &block) + insert_before(target, *args, &block) + delete(target) + end + + def use(*args, &block) + middleware = Middleware.new(*args, &block) + push(middleware) + end + + def active + find_all { |middleware| middleware.active? } + end + + def build(app) + active.reverse.inject(app) { |a, e| e.build(a) } + end + end +end diff --git a/actionpack/lib/action_dispatch/utils/uploaded_file.rb b/actionpack/lib/action_dispatch/utils/uploaded_file.rb new file mode 100644 index 0000000000..97dffa089f --- /dev/null +++ b/actionpack/lib/action_dispatch/utils/uploaded_file.rb @@ -0,0 +1,44 @@ +module ActionDispatch + module UploadedFile + def self.included(base) + base.class_eval do + attr_accessor :original_path, :content_type + alias_method :local_path, :path + end + end + + def self.extended(object) + object.class_eval do + attr_accessor :original_path, :content_type + alias_method :local_path, :path + end + end + + # Take the basename of the upload's original filename. + # This handles the full Windows paths given by Internet Explorer + # (and perhaps other broken user agents) without affecting + # those which give the lone filename. + # The Windows regexp is adapted from Perl's File::Basename. + def original_filename + unless defined? @original_filename + @original_filename = + unless original_path.blank? + if original_path =~ /^(?:.*[:\\\/])?(.*)/m + $1 + else + File.basename original_path + end + end + end + @original_filename + end + end + + class UploadedStringIO < StringIO + include UploadedFile + end + + class UploadedTempfile < Tempfile + include UploadedFile + end +end diff --git a/actionpack/lib/action_dispatch/utils/url_encoded_pair_parser.rb b/actionpack/lib/action_dispatch/utils/url_encoded_pair_parser.rb new file mode 100644 index 0000000000..f2e832a977 --- /dev/null +++ b/actionpack/lib/action_dispatch/utils/url_encoded_pair_parser.rb @@ -0,0 +1,155 @@ +module ActionDispatch + class UrlEncodedPairParser < StringScanner #:nodoc: + class << self + def parse_query_parameters(query_string) + return {} if query_string.blank? + + pairs = query_string.split('&').collect do |chunk| + next if chunk.empty? + key, value = chunk.split('=', 2) + next if key.empty? + value = value.nil? ? nil : CGI.unescape(value) + [ CGI.unescape(key), value ] + end.compact + + new(pairs).result + end + + def parse_hash_parameters(params) + parser = new + + params = params.dup + until params.empty? + for key, value in params + if key.blank? + params.delete(key) + elsif value.is_a?(Array) + parser.parse(key, get_typed_value(value.shift)) + params.delete(key) if value.empty? + else + parser.parse(key, get_typed_value(value)) + params.delete(key) + end + end + end + + parser.result + end + + private + def get_typed_value(value) + case value + when String + value + when NilClass + '' + when Array + value.map { |v| get_typed_value(v) } + when Hash + if value.has_key?(:tempfile) && value[:filename].any? + upload = value[:tempfile] + upload.extend(UploadedFile) + upload.original_path = value[:filename] + upload.content_type = value[:type] + upload + else + nil + end + else + raise "Unknown form value: #{value.inspect}" + end + end + end + + attr_reader :top, :parent, :result + + def initialize(pairs = []) + super('') + @result = {} + pairs.each { |key, value| parse(key, value) } + end + + KEY_REGEXP = %r{([^\[\]=&]+)} + BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} + + # Parse the query string + def parse(key, value) + self.string = key + @top, @parent = result, nil + + # First scan the bare key + key = scan(KEY_REGEXP) or return + key = post_key_check(key) + + # Then scan as many nestings as present + until eos? + r = scan(BRACKETED_KEY_REGEXP) or return + key = self[1] + key = post_key_check(key) + end + + bind(key, value) + end + + private + # After we see a key, we must look ahead to determine our next action. Cases: + # + # [] follows the key. Then the value must be an array. + # = follows the key. (A value comes next) + # & or the end of string follows the key. Then the key is a flag. + # otherwise, a hash follows the key. + def post_key_check(key) + if scan(/\[\]/) # a[b][] indicates that b is an array + container(key, Array) + nil + elsif check(/\[[^\]]/) # a[b] indicates that a is a hash + container(key, Hash) + nil + else # End of key? We do nothing. + key + end + end + + # Add a container to the stack. + def container(key, klass) + type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) + value = bind(key, klass.new) + type_conflict! klass, value unless value.is_a?(klass) + push(value) + end + + # Push a value onto the 'stack', which is actually only the top 2 items. + def push(value) + @parent, @top = @top, value + end + + # Bind a key (which may be nil for items in an array) to the provided value. + def bind(key, value) + if top.is_a? Array + if key + if top[-1].is_a?(Hash) && ! top[-1].key?(key) + top[-1][key] = value + else + top << {key => value}.with_indifferent_access + end + push top.last + return top[key] + else + top << value + return value + end + elsif top.is_a? Hash + key = CGI.unescape(key) + parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) + top[key] ||= value + return top[key] + else + raise ArgumentError, "Don't know what to do: top is #{top.inspect}" + end + end + + def type_conflict!(klass, value) + raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" + end + end +end -- cgit v1.2.3