diff options
Diffstat (limited to 'actionpack/lib/action_controller/dispatch')
22 files changed, 2065 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/dispatch/dispatcher.rb b/actionpack/lib/action_controller/dispatch/dispatcher.rb new file mode 100644 index 0000000000..714e270781 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/dispatcher.rb @@ -0,0 +1,116 @@ +module ActionController + # Dispatches requests to the appropriate controller and takes care of + # reloading the app after each request when Dependencies.load? is true. + class Dispatcher + class << self + def define_dispatcher_callbacks(cache_classes) + unless cache_classes + # Development mode callbacks + before_dispatch :reload_application + after_dispatch :cleanup_application + + ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false + end + + if defined?(ActiveRecord) + after_dispatch :checkin_connections + to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers } + end + + after_dispatch :flush_logger if Base.logger && Base.logger.respond_to?(:flush) + + to_prepare do + I18n.reload! + end + end + + # DEPRECATE: Remove CGI support + def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout) + new(output).dispatch_cgi(cgi, session_options) + end + + # Add a preparation callback. Preparation callbacks are run before every + # request in development mode, and before the first request in production + # mode. + # + # An optional identifier may be supplied for the callback. If provided, + # to_prepare may be called again with the same identifier to replace the + # existing callback. Passing an identifier is a suggested practice if the + # code adding a preparation block may be reloaded. + def to_prepare(identifier = nil, &block) + @prepare_dispatch_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new + callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier) + @prepare_dispatch_callbacks.replace_or_append!(callback) + end + end + + cattr_accessor :middleware + self.middleware = MiddlewareStack.new do |middleware| + middlewares = File.join(File.dirname(__FILE__), "rack", "middlewares.rb") + middleware.instance_eval(File.read(middlewares)) + end + + include ActiveSupport::Callbacks + define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch + + # DEPRECATE: Remove arguments, since they are only used by CGI + def initialize(output = $stdout, request = nil, response = nil) + @output = output + @app = @@middleware.build(lambda { |env| self.dup._call(env) }) + end + + def dispatch + begin + run_callbacks :before_dispatch + Routing::Routes.call(@env) + rescue Exception => exception + if controller ||= (::ApplicationController rescue Base) + controller.call_with_exception(@env, exception).to_a + else + raise exception + end + ensure + run_callbacks :after_dispatch, :enumerator => :reverse_each + end + end + + # DEPRECATE: Remove CGI support + def dispatch_cgi(cgi, session_options) + CGIHandler.dispatch_cgi(self, cgi, @output) + end + + def call(env) + @app.call(env) + end + + def _call(env) + @env = env + dispatch + end + + def reload_application + # Run prepare callbacks before every request in development mode + run_callbacks :prepare_dispatch + + Routing::Routes.reload + end + + # Cleanup the application by clearing out loaded classes so they can + # be reloaded on the next request without restarting the server. + def cleanup_application + ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord) + ActiveSupport::Dependencies.clear + ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord) + end + + def flush_logger + Base.logger.flush + end + + def checkin_connections + # Don't return connection (and peform implicit rollback) if this request is a part of integration test + return if @env.key?("rack.test") + ActiveRecord::Base.clear_active_connections! + end + end +end diff --git a/actionpack/lib/action_controller/dispatch/params_parser.rb b/actionpack/lib/action_controller/dispatch/params_parser.rb new file mode 100644 index 0000000000..d269fe07fa --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/params_parser.rb @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000000..567581142c --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/rack/failsafe.rb @@ -0,0 +1,52 @@ +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 + "<html><body><h1>500 Internal Server Error</h1></body></html>" + 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 new file mode 100644 index 0000000000..c50762216e --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/rack/lock.rb @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..dbc2fda41e --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb @@ -0,0 +1,109 @@ +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 new file mode 100644 index 0000000000..f9cfc2b18e --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/rack/middlewares.rb @@ -0,0 +1,21 @@ +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 new file mode 100755 index 0000000000..f8c77241b9 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/request.rb @@ -0,0 +1,492 @@ +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 + # <tt>:get</tt>. 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 <tt>:post</tt>. (Unlike #request_method, this + # method returns <tt>:get</tt> 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 <tt>request.method == :get</tt>. + def get? + method == :get + end + + # Is this a POST request? Equivalent to <tt>request.method == :post</tt>. + def post? + request_method == :post + end + + # Is this a PUT request? Equivalent to <tt>request.method == :put</tt>. + def put? + request_method == :put + end + + # Is this a DELETE request? Equivalent to <tt>request.method == :delete</tt>. + def delete? + request_method == :delete + end + + # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>, + # 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 <tt>ActionController::Base.use_accept_header</tt> + + 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 <tt>:format</tt> parameter of the request. + # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt> + # otherwise. + def template_format + parameter_format = parameters[:format] + + if parameter_format + parameter_format + elsif xhr? + :js + else + :html + end + end + + 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(<<EOM) +IP spoofing attack?! +HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} +HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect} +EOM + end + + return @env['HTTP_CLIENT_IP'] + end + + if remote_ips + while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip + 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 <tt>tld_length</tt>, 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 <tt>["dev", "www"]</tt> would be + # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, + # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> + # 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 <tt>path_parameters</tt> 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 <tt>symbolized_path_parameters</tt> 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 new file mode 100644 index 0000000000..d1739ef4d0 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/request_parser.rb @@ -0,0 +1,315 @@ +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 new file mode 100644 index 0000000000..0293e62fc7 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/rescue.rb @@ -0,0 +1,179 @@ +module ActionController #:nodoc: + # Actions that fail to perform as expected throw exceptions. These + # exceptions can either be rescued for the public view (with a nice + # user-friendly explanation) or for the developers view (with tons of + # debugging information). The developers view is already implemented by + # the Action Controller, but the public view should be tailored to your + # specific application. + # + # The default behavior for public exceptions is to render a static html + # file with the name of the error code thrown. If no such file exists, an + # empty response is sent with the correct status code. + # + # You can override what constitutes a local request by overriding the + # <tt>local_request?</tt> method in your own controller. Custom rescue + # behavior is achieved by overriding the <tt>rescue_action_in_public</tt> + # and <tt>rescue_action_locally</tt> methods. + module Rescue + LOCALHOST = '127.0.0.1'.freeze + + DEFAULT_RESCUE_RESPONSE = :internal_server_error + DEFAULT_RESCUE_RESPONSES = { + 'ActionController::RoutingError' => :not_found, + 'ActionController::UnknownAction' => :not_found, + 'ActiveRecord::RecordNotFound' => :not_found, + 'ActiveRecord::StaleObjectError' => :conflict, + 'ActiveRecord::RecordInvalid' => :unprocessable_entity, + 'ActiveRecord::RecordNotSaved' => :unprocessable_entity, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity + } + + DEFAULT_RESCUE_TEMPLATE = 'diagnostics' + DEFAULT_RESCUE_TEMPLATES = { + 'ActionView::MissingTemplate' => 'missing_template', + 'ActionController::RoutingError' => 'routing_error', + 'ActionController::UnknownAction' => 'unknown_action', + 'ActionView::TemplateError' => 'template_error' + } + + RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new( + File.join(File.dirname(__FILE__), "templates")) + + def self.included(base) #:nodoc: + base.cattr_accessor :rescue_responses + base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE) + base.rescue_responses.update DEFAULT_RESCUE_RESPONSES + + base.cattr_accessor :rescue_templates + base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE) + base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES + + base.extend(ClassMethods) + base.send :include, ActiveSupport::Rescuable + + base.class_eval do + alias_method_chain :perform_action, :rescue + end + end + + 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 + new.process(request, response, :rescue_action, exception) + end + end + + protected + # Exception handler called when the performance of an action raises + # an exception. + def rescue_action(exception) + rescue_with_handler(exception) || + rescue_action_without_handler(exception) + end + + # Overwrite to implement custom logging of errors. By default + # logs as fatal. + def log_error(exception) #:doc: + ActiveSupport::Deprecation.silence do + if ActionView::TemplateError === exception + logger.fatal(exception.to_s) + else + logger.fatal( + "\n#{exception.class} (#{exception.message}):\n " + + clean_backtrace(exception).join("\n ") + "\n\n" + ) + end + end + end + + # Overwrite to implement public exception handling (for requests + # answering false to <tt>local_request?</tt>). By default will call + # render_optional_error_file. Override this method to provide more + # user friendly error messages. + def rescue_action_in_public(exception) #:doc: + render_optional_error_file response_code_for_rescue(exception) + end + + # Attempts to render a static error page based on the + # <tt>status_code</tt> thrown, or just return headers if no such file + # exists. For example, if a 500 error is being handled Rails will first + # attempt to render the file at <tt>public/500.html</tt>. If the file + # doesn't exist, the body of the response will be left empty. + def render_optional_error_file(status_code) + status = interpret_status(status_code) + path = "#{Rails.public_path}/#{status.to_s[0,3]}.html" + if File.exist?(path) + render :file => path, :status => status, :content_type => Mime::HTML + else + head status + end + end + + # True if the request came from localhost, 127.0.0.1. Override this + # method if you wish to redefine the meaning of a local request to + # include remote IP addresses or other criteria. + def local_request? #:doc: + request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST + end + + # Render detailed diagnostics for unhandled exceptions rescued from + # a controller action. + def rescue_action_locally(exception) + @template.instance_variable_set("@exception", exception) + @template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH) + @template.instance_variable_set("@contents", + @template._render_template(template_path_for_local_rescue(exception))) + + response.content_type = Mime::HTML + response.status = interpret_status(response_code_for_rescue(exception)) + + content = @template._render_template(rescues_path("layout")) + render_for_text(content) + end + + def rescue_action_without_handler(exception) + log_error(exception) if logger + erase_results if performed? + + # Let the exception alter the response if it wants. + # For example, MethodNotAllowed sets the Allow header. + if exception.respond_to?(:handle_response!) + exception.handle_response!(response) + end + + if consider_all_requests_local || local_request? + rescue_action_locally(exception) + else + rescue_action_in_public(exception) + end + end + + private + def perform_action_with_rescue #:nodoc: + perform_action_without_rescue + rescue Exception => exception + rescue_action(exception) + end + + def rescues_path(template_name) + RESCUES_TEMPLATE_PATH.find_template("rescues/#{template_name}.erb") + end + + def template_path_for_local_rescue(exception) + rescues_path(rescue_templates[exception.class.name]) + end + + def response_code_for_rescue(exception) + rescue_responses[exception.class.name] + end + + def clean_backtrace(exception) + defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? + Rails.backtrace_cleaner.clean(exception.backtrace) : + exception.backtrace + end + end +end diff --git a/actionpack/lib/action_controller/dispatch/response.rb b/actionpack/lib/action_controller/dispatch/response.rb new file mode 100644 index 0000000000..27860a6207 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/response.rb @@ -0,0 +1,255 @@ +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 = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>" + 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 new file mode 100644 index 0000000000..36f655c51e --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/rewindable_input.rb @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000000..4977c79491 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/status_codes.rb @@ -0,0 +1,88 @@ +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/templates/rescues/_request_and_response.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb new file mode 100644 index 0000000000..64b34650b1 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb @@ -0,0 +1,24 @@ +<% unless @exception.blamed_files.blank? %> + <% if (hide = @exception.blamed_files.length > 8) %> + <a href="#" onclick="document.getElementById('blame_trace').style.display='block'; return false;">Show blamed files</a> + <% end %> + <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%=h @exception.describe_blame %></code></pre> +<% end %> + +<% + clean_params = request.parameters.clone + clean_params.delete("action") + clean_params.delete("controller") + + request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") +%> + +<h2 style="margin-top: 30px">Request</h2> +<p><b>Parameters</b>: <pre><%=h request_dump %></pre></p> + +<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p> +<div id="session_dump" style="display:none"><%= debug(request.session.instance_variable_get("@data")) %></div> + + +<h2 style="margin-top: 30px">Response</h2> +<p><b>Headers</b>: <pre><%=h response ? response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb new file mode 100644 index 0000000000..bb2d8375bd --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb @@ -0,0 +1,26 @@ +<% + traces = [ + ["Application Trace", @exception.application_backtrace], + ["Framework Trace", @exception.framework_backtrace], + ["Full Trace", @exception.clean_backtrace] + ] + names = traces.collect {|name, trace| name} +%> + +<p><code>RAILS_ROOT: <%= defined?(RAILS_ROOT) ? RAILS_ROOT : "unset" %></code></p> + +<div id="traces"> + <% names.each do |name| %> + <% + show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';" + hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"} + %> + <a href="#" onclick="<%= hide %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> + <% end %> + + <% traces.each do |name, trace| %> + <div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;"> + <pre><code><%= trace.join "\n" %></code></pre> + </div> + <% end %> +</div> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb new file mode 100644 index 0000000000..95be64511d --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb @@ -0,0 +1,10 @@ +<h1> + <%=h @exception.class.to_s %> + <% if request.parameters['controller'] %> + in <%=h request.parameters['controller'].humanize %>Controller<% if request.parameters['action'] %>#<%=h request.parameters['action'] %><% end %> + <% end %> +</h1> +<pre><%=h @exception.clean_message %></pre> + +<%= @template._render_template(@rescues_path.find_template("rescues/_trace.erb")) %> +<%= @template._render_template(@rescues_path.find_template("rescues/_request_and_response.erb")) %>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb new file mode 100644 index 0000000000..4a04742e40 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb @@ -0,0 +1,29 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Action Controller: Exception caught</title> + <style> + body { background-color: #fff; color: #333; } + + body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; + } + + pre { + background-color: #eee; + padding: 10px; + font-size: 11px; + } + + a { color: #000; } + a:visited { color: #666; } + a:hover { color: #fff; background-color:#000; } + </style> +</head> +<body> + +<%= @contents %> + +</body> +</html>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb new file mode 100644 index 0000000000..dbfdf76947 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb @@ -0,0 +1,2 @@ +<h1>Template is missing</h1> +<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb new file mode 100644 index 0000000000..ccfa858cce --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb @@ -0,0 +1,10 @@ +<h1>Routing Error</h1> +<p><pre><%=h @exception.message %></pre></p> +<% unless @exception.failures.empty? %><p> + <h2>Failure reasons:</h2> + <ol> + <% @exception.failures.each do |route, reason| %> + <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li> + <% end %> + </ol> +</p><% end %> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb new file mode 100644 index 0000000000..2e34e03bd5 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb @@ -0,0 +1,21 @@ +<h1> + <%=h @exception.original_exception.class.to_s %> in + <%=h request.parameters["controller"].capitalize if request.parameters["controller"]%>#<%=h request.parameters["action"] %> +</h1> + +<p> + Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised: + <pre><code><%=h @exception.message %></code></pre> +</p> + +<p>Extracted source (around line <b>#<%=h @exception.line_number %></b>): +<pre><code><%=h @exception.source_extract %></code></pre></p> + +<p><%=h @exception.sub_template_message %></p> + +<% @real_exception = @exception + @exception = @exception.original_exception || @exception %> +<%= render :file => @rescues_path["rescues/_trace.erb"] %> +<% @exception = @real_exception %> + +<%= render :file => @rescues_path["rescues/_request_and_response.erb"] %> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb new file mode 100644 index 0000000000..683379da10 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb @@ -0,0 +1,2 @@ +<h1>Unknown action</h1> +<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_controller/dispatch/uploaded_file.rb b/actionpack/lib/action_controller/dispatch/uploaded_file.rb new file mode 100644 index 0000000000..376ba3621a --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/uploaded_file.rb @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000000..57594c4259 --- /dev/null +++ b/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb @@ -0,0 +1,155 @@ +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 |