diff options
author | Joshua Peek <josh@joshpeek.com> | 2009-05-29 16:06:21 -0500 |
---|---|---|
committer | Joshua Peek <josh@joshpeek.com> | 2009-05-29 16:06:21 -0500 |
commit | 69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3 (patch) | |
tree | 044c2131cc87d21ee54027511aae2b7f2d2ae26a /actionpack/lib/action_dispatch | |
parent | 5f3f100ce2d689480da85abc88e5e940cf90189e (diff) | |
parent | 5ec2c7dc29b36d85b2658465b8a979deb0529d7e (diff) | |
download | rails-69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3.tar.gz rails-69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3.tar.bz2 rails-69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3.zip |
Merge branch 'master' into active_model
Conflicts:
activemodel/lib/active_model/core.rb
activemodel/test/cases/state_machine/event_test.rb
activemodel/test/cases/state_machine/state_transition_test.rb
activerecord/lib/active_record/validations.rb
activerecord/test/cases/validations/i18n_validation_test.rb
activeresource/lib/active_resource.rb
activeresource/test/abstract_unit.rb
Diffstat (limited to 'actionpack/lib/action_dispatch')
91 files changed, 3041 insertions, 345 deletions
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 02ad7f7d94..25156a4c75 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,8 +1,9 @@ require 'set' +require 'active_support/core_ext/class/attribute_accessors' module Mime SET = [] - EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + EXTENSION_LOOKUP = {} LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } def self.[](type) diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 2d7fba1173..7c28cac419 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -1,9 +1,9 @@ # Build list of Mime types for HTTP responses # http://www.iana.org/assignments/media-types/ +Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml ) 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 diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 523ab32b35..140feb9a68 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -3,6 +3,9 @@ require 'stringio' require 'strscan' require 'active_support/memoizable' +require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/tap' module ActionDispatch class Request < Rack::Request @@ -31,7 +34,7 @@ module ActionDispatch # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS # constant above, an UnknownHttpMethod exception is raised. def request_method - @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(ActionController::UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + HTTP_METHOD_LOOKUP[super] || raise(ActionController::UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") end # Returns the HTTP request \method used for action processing as a @@ -85,7 +88,7 @@ module ActionDispatch # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - @content_type ||= begin + @env["action_dispatch.request.content_type"] ||= begin if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ Mime::Type.lookup($1.strip.downcase) else @@ -93,10 +96,14 @@ module ActionDispatch end end end - + + def media_type + content_type.to_s + end + # Returns the accepted MIME type for the request. def accepts - @accepts ||= begin + @env["action_dispatch.request.accepts"] ||= begin header = @env['HTTP_ACCEPT'].to_s.strip fallback = xhr? ? Mime::JS : Mime::HTML @@ -156,7 +163,7 @@ module ActionDispatch # 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 ||= + @env["action_dispatch.request.format"] ||= if parameters[:format] Mime[parameters[:format]] elsif ActionController::Base.use_accept_header && !(accepts == ONLY_ALL) @@ -167,12 +174,23 @@ module ActionDispatch end def formats - @formats = - if ActionController::Base.use_accept_header - Array(Mime[parameters[:format]] || accepts) + if ActionController::Base.use_accept_header + if param = parameters[:format] + Array.wrap(Mime[param]) else - [format] + accepts.dup + end.tap do |ret| + if defined?(ActionController::Http) + if ret == ONLY_ALL + ret.replace Mime::SET + elsif all = ret.index(Mime::ALL) + ret.delete_at(all) && ret.insert(all, *Mime::SET) + end + end end + else + [format] + Mime::SET + end end # Sets the \format by string extension, which can be used to force custom formats @@ -188,7 +206,7 @@ module ActionDispatch # end def format=(extension) parameters[:format] = extension.to_s - @format = Mime::Type.lookup_by_extension(parameters[:format]) + @env["action_dispatch.request.format"] = Mime::Type.lookup_by_extension(parameters[:format]) end # Returns a symbolized version of the <tt>:format</tt> parameter of the request. @@ -324,6 +342,10 @@ EOM port == standard_port ? '' : ":#{port}" end + def server_port + @env['SERVER_PORT'].to_i + 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) @@ -344,7 +366,7 @@ EOM # Returns the query string, accounting for server idiosyncrasies. def query_string - @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') + @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '') end # Returns the request URI, accounting for server idiosyncrasies. @@ -392,18 +414,19 @@ EOM # Returns both GET and POST \parameters in a single hash. def parameters - @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access + @env["action_dispatch.request.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 + @env.delete("action_dispatch.request.symbolized_path_parameters") + @env.delete("action_dispatch.request.parameters") + @env["action_dispatch.request.path_parameters"] = parameters end # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters - @symbolized_path_parameters ||= path_parameters.symbolize_keys + @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys end # Returns a hash with the \parameters used to form the \path of the request. @@ -413,7 +436,7 @@ EOM # # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters - @env["rack.routing_args"] ||= {} + @env["action_dispatch.request.path_parameters"] ||= {} end # The request body is an IO input stream. If the RAW_POST_DATA environment @@ -433,13 +456,13 @@ EOM # Override Rack's GET method to support indifferent access def GET - @env["action_controller.request.query_parameters"] ||= normalize_parameters(super) + @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) end alias_method :query_parameters, :GET # Override Rack's POST method to support indifferent access def POST - @env["action_controller.request.request_parameters"] ||= normalize_parameters(super) + @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super) end alias_method :request_parameters, :POST @@ -447,29 +470,21 @@ EOM @env['rack.input'] end - def session - @env['rack.session'] ||= {} + def reset_session + self.session_options.delete(:id) + self.session = {} end def session=(session) #:nodoc: @env['rack.session'] = session end - def reset_session - @env['rack.session.options'].delete(:id) - @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 + def flash + session['flash'] || {} end private diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index ecf40b8103..b9db7a4508 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,4 +1,5 @@ require 'digest/md5' +require 'active_support/core_ext/module/delegation' module ActionDispatch # :nodoc: # Represents an HTTP response generated by a controller action. One can use @@ -34,17 +35,31 @@ module ActionDispatch # :nodoc: DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } attr_accessor :request - attr_accessor :session, :assigns, :template, :layout - attr_accessor :redirected_to, :redirected_to_method_params + attr_writer :header + alias_method :headers=, :header= delegate :default_charset, :to => 'ActionController::Base' def initialize super @header = Rack::Utils::HeaderHash.new(DEFAULT_HEADERS) - @session, @assigns = [], [] end + # The response code of the request + def response_code + status.to_s[0,3].to_i rescue 0 + end + + # Returns a String to ensure compatibility with Net::HTTPResponse + def code + status.to_s.split(' ')[0] + end + + def message + status.to_s.split(' ',2)[1] || StatusCodes::STATUS_CODES[response_code] + end + alias_method :status_message, :message + def body str = '' each { |part| str << part.to_s } @@ -53,7 +68,7 @@ module ActionDispatch # :nodoc: def body=(body) @body = - if body.is_a?(String) + if body.respond_to?(:to_str) [body] else body @@ -64,9 +79,14 @@ module ActionDispatch # :nodoc: @body end - def location; headers['Location'] end - def location=(url) headers['Location'] = url end + def location + headers['Location'] + end + alias_method :redirect_url, :location + def location=(url) + headers['Location'] = url + end # Sets the HTTP response's content MIME type. For example, in the controller # you could write this: @@ -137,19 +157,20 @@ module ActionDispatch # :nodoc: 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? + if type = headers['Content-Type'] || headers['type'] + unless type =~ /charset=/ || sending_file? + headers['Content-Type'] = "#{type}; charset=#{default_charset}" + end + else + type = Mime::HTML.to_s + type += "; charset=#{default_charset}" unless sending_file? + headers['Content-Type'] = type + end end def prepare! @@ -165,10 +186,8 @@ module ActionDispatch # :nodoc: if @body.respond_to?(:call) @writer = lambda { |x| callback.call(x) } @body.call(self, self) - elsif @body.is_a?(String) - callback.call(@body) else - @body.each(&callback) + @body.each { |part| callback.call(part.to_s) } end @writer = callback @@ -192,6 +211,23 @@ module ActionDispatch # :nodoc: super(key, value) end + # Returns the response cookies, converted to a Hash of (name => value) pairs + # + # assert_equal 'AuthorOfNewPage', r.cookies['author'] + def cookies + cookies = {} + if header = headers['Set-Cookie'] + header = header.split("\n") if header.respond_to?(:to_str) + header.each do |cookie| + if pair = cookie.split(';').first + key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) } + cookies[key] = value + end + end + end + cookies + end + private def handle_conditional_get! if etag? || last_modified? @@ -245,7 +281,13 @@ module ActionDispatch # :nodoc: end def convert_cookies! - headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact + headers['Set-Cookie'] = + if header = headers['Set-Cookie'] + header = header.split("\n") if header.respond_to?(:to_str) + header.compact + else + [] + end end end end diff --git a/actionpack/lib/action_dispatch/http/status_codes.rb b/actionpack/lib/action_dispatch/http/status_codes.rb index 830de2a6db..5bac842ec1 100644 --- a/actionpack/lib/action_dispatch/http/status_codes.rb +++ b/actionpack/lib/action_dispatch/http/status_codes.rb @@ -1,3 +1,5 @@ +require 'active_support/inflector' + module ActionDispatch module StatusCodes #:nodoc: STATUS_CODES = Rack::Utils::HTTP_STATUS_CODES.merge({ @@ -16,7 +18,7 @@ module ActionDispatch # :created or :not_implemented) into its corresponding HTTP status # code (like 200 or 501). SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) { |hash, (code, message)| - hash[message.gsub(/ /, "").underscore.to_sym] = code + hash[ActiveSupport::Inflector.underscore(message.gsub(/ /, "")).to_sym] = code hash }.freeze @@ -37,4 +39,4 @@ module ActionDispatch end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb new file mode 100644 index 0000000000..0a2b4cf5f7 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -0,0 +1,40 @@ +module ActionDispatch + class Callbacks + include ActiveSupport::Callbacks + define_callbacks :prepare, :before, :after + + class << self + # DEPRECATED + alias_method :prepare_dispatch, :prepare + alias_method :before_dispatch, :before + alias_method :after_dispatch, :after + 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 self.to_prepare(identifier = nil, &block) + @prepare_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new + callback = ActiveSupport::Callbacks::Callback.new(:prepare, block, :identifier => identifier) + @prepare_callbacks.replace_or_append!(callback) + end + + def initialize(app, prepare_each_request = false) + @app, @prepare_each_request = app, prepare_each_request + run_callbacks :prepare + end + + def call(env) + run_callbacks :before + run_callbacks :prepare if @prepare_each_request + @app.call(env) + ensure + run_callbacks :after, :enumerator => :reverse_each + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/failsafe.rb b/actionpack/lib/action_dispatch/middleware/failsafe.rb deleted file mode 100644 index 7379a696aa..0000000000 --- a/actionpack/lib/action_dispatch/middleware/failsafe.rb +++ /dev/null @@ -1,52 +0,0 @@ -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 - "<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_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 6df572268c..e83cf9236b 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -1,3 +1,5 @@ +require 'active_support/json' + module ActionDispatch class ParamsParser ActionController::Base.param_parsers[Mime::XML] = :xml_simple @@ -9,7 +11,7 @@ module ActionDispatch def call(env) if params = parse_formatted_parameters(env) - env["action_controller.request.request_parameters"] = params + env["action_dispatch.request.request_parameters"] = params end @app.call(env) @@ -30,16 +32,14 @@ module ActionDispatch 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 + request.body.size == 0 ? {} : Hash.from_xml(request.body).with_indifferent_access when :yaml - YAML.load(request.raw_post) + YAML.load(request.body) when :json - body = request.raw_post - if body.blank? + if request.body.size == 0 {} else - data = ActiveSupport::JSON.decode(body) + data = ActiveSupport::JSON.decode(request.body) data = {:_json => data} unless data.is_a?(Hash) data.with_indifferent_access end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb deleted file mode 100644 index 67313e30e4..0000000000 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActionDispatch - class Reloader - def initialize(app) - @app = app - end - - def call(env) - ActionController::Dispatcher.reload_application - @app.call(env) - ensure - ActionController::Dispatcher.cleanup_application - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/rescue.rb b/actionpack/lib/action_dispatch/middleware/rescue.rb new file mode 100644 index 0000000000..1456825526 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/rescue.rb @@ -0,0 +1,14 @@ +module ActionDispatch + class Rescue + def initialize(app, rescuer) + @app, @rescuer = app, rescuer + end + + def call(env) + @app.call(env) + rescue Exception => exception + env['action_dispatch.rescue.exception'] = exception + @rescuer.call(env) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/rewindable_input.rb b/actionpack/lib/action_dispatch/middleware/rewindable_input.rb deleted file mode 100644 index c818f28cce..0000000000 --- a/actionpack/lib/action_dispatch/middleware/rewindable_input.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActionDispatch - class RewindableInput - def initialize(app) - @app = app - end - - def call(env) - begin - env['rack.input'].rewind - rescue NoMethodError, Errno::ESPIPE - # Handles exceptions raised by input streams that cannot be rewound - # such as when using plain CGI under Apache - env['rack.input'] = StringIO.new(env['rack.input'].read) - end - - @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 index 6c039cf62d..6d109f4624 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -15,6 +15,7 @@ module ActionDispatch @by = by @env = env @loaded = false + @updated = false end def session_id @@ -26,12 +27,13 @@ module ActionDispatch def [](key) load! unless @loaded - super + super(key.to_s) end def []=(key, value) load! unless @loaded - super + super(key.to_s, value) + @updated = true end def to_hash @@ -40,6 +42,24 @@ module ActionDispatch h end + def update(hash = nil) + if hash.nil? + ActiveSupport::Deprecation.warn('use replace instead', caller) + replace({}) + else + super(hash.stringify_keys) + end + end + + def delete(key = nil) + if key.nil? + ActiveSupport::Deprecation.warn('use clear instead', caller) + clear + else + super(key.to_s) + end + end + def data ActiveSupport::Deprecation.warn( "ActionController::Session::AbstractStore::SessionHash#data " + @@ -47,6 +67,10 @@ module ActionDispatch to_hash end + def close + ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller) + end + def inspect load! unless @loaded super @@ -57,11 +81,15 @@ module ActionDispatch @loaded end + def updated? + @updated + end + def load! stale_session_check! do id, session = @by.send(:load_session, @env) (@env[ENV_SESSION_OPTIONS_KEY] ||= {})[:id] = id - replace(session) + replace(session.stringify_keys) @loaded = true end end @@ -74,7 +102,7 @@ module ActionDispatch # Note that the regexp does not allow $1 to end with a ':' $1.constantize rescue LoadError, NameError => const_error - raise ActionController::SessionRestoreError, "Session contains objects whose class definition isn\\'t available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: \#{const_error.message} [\#{const_error.class}])\n" + raise ActionController::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" end retry @@ -125,7 +153,10 @@ module ActionDispatch options = env[ENV_SESSION_OPTIONS_KEY] if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after] - session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) + if session_data.is_a?(AbstractStore::SessionHash) + session_data.send(:load!) if !session_data.send(:loaded?) + return response if !session_data.send(:updated?) + end sid = options[:id] || generate_sid diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 433c4cc070..547a2d2062 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -143,7 +143,8 @@ module ActionDispatch request = Rack::Request.new(env) session_data = request.cookies[@key] data = unmarshal(session_data) || persistent_session_id!({}) - [data[:session_id], data] + data.stringify_keys! + [data["session_id"], data] end # Marshal a session hash into safe cookie data. Include an integrity hash. @@ -206,12 +207,12 @@ module ActionDispatch end def inject_persistent_session_id(data) - requires_session_id?(data) ? { :session_id => generate_sid } : {} + requires_session_id?(data) ? { "session_id" => generate_sid } : {} end def requires_session_id?(data) if data - data.respond_to?(:key?) && !data.key?(:session_id) + data.respond_to?(:key?) && !data.key?("session_id") else true end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb new file mode 100644 index 0000000000..bfff307669 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -0,0 +1,141 @@ +module ActionDispatch + class ShowExceptions + include StatusCodes + + LOCALHOST = '127.0.0.1'.freeze + + RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') + + cattr_accessor :rescue_responses + @@rescue_responses = Hash.new(:internal_server_error) + @@rescue_responses.update({ + 'ActionController::RoutingError' => :not_found, + # TODO: Clean this up after the switch + ActionController::UnknownAction.name => :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 + }) + + cattr_accessor :rescue_templates + @@rescue_templates = Hash.new('diagnostics') + @@rescue_templates.update({ + 'ActionView::MissingTemplate' => 'missing_template', + 'ActionController::RoutingError' => 'routing_error', + ActionController::UnknownAction.name => 'unknown_action', + 'ActionView::TemplateError' => 'template_error' + }) + + FAILSAFE_RESPONSE = [500, {'Content-Type' => 'text/html'}, + ["<html><body><h1>500 Internal Server Error</h1>" << + "If you are the administrator of this website, then please read this web " << + "application's log file and/or the web server's log file to find out what " << + "went wrong.</body></html>"]] + + def initialize(app, consider_all_requests_local = false) + @app = app + @consider_all_requests_local = consider_all_requests_local + end + + def call(env) + @app.call(env) + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false + render_exception(env, exception) + end + + private + def render_exception(env, exception) + log_error(exception) + + request = Request.new(env) + if @consider_all_requests_local || local_request?(request) + rescue_action_locally(request, exception) + else + rescue_action_in_public(exception) + end + rescue Exception => failsafe_error + $stderr.puts "Error during failsafe response: #{failsafe_error}" + FAILSAFE_RESPONSE + end + + # Render detailed diagnostics for unhandled exceptions rescued from + # a controller action. + def rescue_action_locally(request, exception) + template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], + :request => request, + :exception => exception + ) + file = "rescues/#{@@rescue_templates[exception.class.name]}.erb" + body = template.render(:file => file, :layout => 'rescues/layout.erb') + render(status_code(exception), body) + 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. At first, it will try to render a localized static page. + # For example, if a 500 error is being handled Rails and locale is :da, + # it will first attempt to render the file at <tt>public/500.da.html</tt> + # then attempt to render <tt>public/500.html</tt>. If none of them exist, + # the body of the response will be left empty. + def rescue_action_in_public(exception) + status = status_code(exception) + locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale + path = "#{public_path}/#{status}.html" + + if locale_path && File.exist?(locale_path) + render(status, File.read(locale_path)) + elsif File.exist?(path) + render(status, File.read(path)) + else + render(status, '') + end + end + + # True if the request came from localhost, 127.0.0.1. + def local_request?(request) + request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST + end + + def status_code(exception) + interpret_status(@@rescue_responses[exception.class.name]).to_i + end + + def render(status, body) + [status, {'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s}, [body]] + end + + def public_path + defined?(Rails.public_path) ? Rails.public_path : 'public_path' + end + + def log_error(exception) + return unless logger + + 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 + + def clean_backtrace(exception) + defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? + Rails.backtrace_cleaner.clean(exception.backtrace) : + exception.backtrace + end + + def logger + defined?(Rails.logger) ? Rails.logger : Logger.new($stderr) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index ee5f28d5cb..ade2d6f05e 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -34,8 +34,6 @@ module ActionDispatch else @klass.to_s.constantize end - rescue NameError - @klass end def active? @@ -61,7 +59,7 @@ module ActionDispatch def inspect str = klass.to_s - args.each { |arg| str += ", #{arg.inspect}" } + args.each { |arg| str += ", #{build_args.inspect}" } str end @@ -74,7 +72,6 @@ module ActionDispatch end private - def build_args Array(args).map { |arg| arg.respond_to?(:call) ? arg.call : arg } end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb new file mode 100644 index 0000000000..5224403dab --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb new file mode 100644 index 0000000000..bb2d8375bd --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb new file mode 100644 index 0000000000..693e56270a --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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> + +<%= render :file => "rescues/_trace.erb" %> +<%= render :file => "rescues/_request_and_response.erb" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb new file mode 100644 index 0000000000..6c32fb17b8 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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> + +<%= yield %> + +</body> +</html> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb new file mode 100644 index 0000000000..dbfdf76947 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb new file mode 100644 index 0000000000..ccfa858cce --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb new file mode 100644 index 0000000000..02fa18211d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/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/_trace.erb" %> +<% @exception = @real_exception %> + +<%= render :file => "rescues/_request_and_response.erb" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb new file mode 100644 index 0000000000..683379da10 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb @@ -0,0 +1,2 @@ +<h1>Unknown action</h1> +<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb new file mode 100644 index 0000000000..96f08f2355 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -0,0 +1,8 @@ +module ActionDispatch + module Assertions + %w(response selector tag dom routing model).each do |kind| + require "action_dispatch/testing/assertions/#{kind}" + include const_get("#{kind.camelize}Assertions") + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb new file mode 100644 index 0000000000..9a917f704a --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -0,0 +1,35 @@ +module ActionDispatch + module Assertions + module DomAssertions + # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) + # + # ==== Examples + # + # # assert that the referenced method generates the appropriate HTML string + # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com") + # + def assert_dom_equal(expected, actual, message = "") + expected_dom = HTML::Document.new(expected).root + actual_dom = HTML::Document.new(actual).root + full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s) + + assert_block(full_message) { expected_dom == actual_dom } + end + + # The negated form of +assert_dom_equivalent+. + # + # ==== Examples + # + # # assert that the referenced method does not generate the specified HTML string + # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com") + # + def assert_dom_not_equal(expected, actual, message = "") + expected_dom = HTML::Document.new(expected).root + actual_dom = HTML::Document.new(actual).root + full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s) + + assert_block(full_message) { expected_dom != actual_dom } + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/model.rb b/actionpack/lib/action_dispatch/testing/assertions/model.rb new file mode 100644 index 0000000000..46714418c6 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions/model.rb @@ -0,0 +1,19 @@ +module ActionDispatch + module Assertions + module ModelAssertions + # Ensures that the passed record is valid by Active Record standards and + # returns any error messages if it is not. + # + # ==== Examples + # + # # assert that a newly created record is valid + # model = Model.new + # assert_valid(model) + # + def assert_valid(record) + ::ActiveSupport::Deprecation.warn("assert_valid is deprecated. Use assert record.valid? instead", caller) + assert record.valid?, record.errors.full_messages.join("\n") + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb new file mode 100644 index 0000000000..501a7c4dfb --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -0,0 +1,145 @@ +module ActionDispatch + module Assertions + # A small suite of assertions that test responses from Rails applications. + module ResponseAssertions + # Asserts that the response is one of the following types: + # + # * <tt>:success</tt> - Status code was 200 + # * <tt>:redirect</tt> - Status code was in the 300-399 range + # * <tt>:missing</tt> - Status code was 404 + # * <tt>:error</tt> - Status code was in the 500-599 range + # + # You can also pass an explicit status number like assert_response(501) + # or its symbolic equivalent assert_response(:not_implemented). + # See ActionDispatch::StatusCodes for a full list. + # + # ==== Examples + # + # # assert that the response was a redirection + # assert_response :redirect + # + # # assert that the response code was status code 401 (unauthorized) + # assert_response 401 + # + def assert_response(type, message = nil) + validate_request! + + if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?") + 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 == ActionDispatch::StatusCodes::SYMBOL_TO_STATUS_CODE[type] + assert_block("") { true } # to count the assertion + else + assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false } + end + end + + # Assert that the redirection options passed in match those of the redirect called in the latest action. + # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also + # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on. + # + # ==== Examples + # + # # assert that the redirection was to the "index" action on the WeblogController + # assert_redirected_to :controller => "weblog", :action => "index" + # + # # assert that the redirection was to the named route login_url + # assert_redirected_to login_url + # + # # assert that the redirection was to the url for @customer + # assert_redirected_to @customer + # + def assert_redirected_to(options = {}, message=nil) + validate_request! + + assert_response(:redirect, message) + return true if options == @response.location + + redirected_to_after_normalisation = normalize_argument_to_redirection(@response.location) + options_after_normalisation = normalize_argument_to_redirection(options) + + if redirected_to_after_normalisation != options_after_normalisation + flunk "Expected response to be a redirect to <#{options_after_normalisation}> but was a redirect to <#{redirected_to_after_normalisation}>" + end + end + + # Asserts that the request was rendered with the appropriate template file or partials + # + # ==== Examples + # + # # assert that the "new" view template was rendered + # assert_template "new" + # + # # assert that the "_customer" partial was rendered twice + # assert_template :partial => '_customer', :count => 2 + # + # # assert that no partials were rendered + # assert_template :partial => false + # + def assert_template(options = {}, message = nil) + validate_request! + + case options + when NilClass, String + rendered = (@controller.template.rendered[:template] || []).map { |t| t.identifier } + msg = build_message(message, + "expecting <?> but rendering with <?>", + options, rendered.join(', ')) + assert_block(msg) do + if options.nil? + @controller.template.rendered[:template].blank? + else + rendered.any? { |t| t.match(options) } + end + end + when Hash + if expected_partial = options[:partial] + partials = @controller.template.rendered[:partials] + if expected_count = options[:count] + found = partials.detect { |p, _| p.identifier.match(expected_partial) } + actual_count = found.nil? ? 0 : found.second + msg = build_message(message, + "expecting ? to be rendered ? time(s) but rendered ? time(s)", + expected_partial, expected_count, actual_count) + assert(actual_count == expected_count.to_i, msg) + else + msg = build_message(message, + "expecting partial <?> but action rendered <?>", + options[:partial], partials.keys) + assert(partials.keys.any? { |p| p.identifier.match(expected_partial) }, msg) + end + else + assert @controller.template.rendered[:partials].empty?, + "Expected no partials to be rendered" + end + end + end + + private + # Proxy to to_param if the object will respond to it. + def parameterize(value) + value.respond_to?(:to_param) ? value.to_param : value + end + + def normalize_argument_to_redirection(fragment) + after_routing = @controller.url_for(fragment) + if after_routing =~ %r{^\w+://.*} + after_routing + else + # FIXME - this should probably get removed. + if after_routing.first != '/' + after_routing = '/' + after_routing + end + @request.protocol + @request.host_with_port + after_routing + end + end + + def validate_request! + unless @request.is_a?(ActionDispatch::Request) + raise ArgumentError, "@request must be an ActionDispatch::Request" + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb new file mode 100644 index 0000000000..89d1a49403 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -0,0 +1,142 @@ +module ActionDispatch + module Assertions + # Suite of assertions to test routes generated by Rails and the handling of requests made to them. + module RoutingAssertions + # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) + # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+. + # + # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes + # requiring a specific HTTP method. The hash should contain a :path with the incoming request path + # and a :method containing the required HTTP verb. + # + # # assert that POSTing to /items will call the create action on ItemsController + # assert_recognizes {:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post} + # + # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used + # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the + # extras argument, appending the query string on the path directly will not work. For example: + # + # # assert that a path of '/items/list/1?view=print' returns the correct options + # assert_recognizes {:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" } + # + # The +message+ parameter allows you to pass in an error message that is displayed upon failure. + # + # ==== Examples + # # Check the default route (i.e., the index action) + # assert_recognizes {:controller => 'items', :action => 'index'}, 'items' + # + # # Test a specific action + # assert_recognizes {:controller => 'items', :action => 'list'}, 'items/list' + # + # # Test an action with a parameter + # assert_recognizes {:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1' + # + # # Test a custom route + # assert_recognizes {:controller => 'items', :action => 'show', :id => '1'}, 'view/item1' + # + # # Check a Simply RESTful generated route + # assert_recognizes list_items_url, 'items/list' + def assert_recognizes(expected_options, path, extras={}, message=nil) + if path.is_a? Hash + request_method = path[:method] + path = path[:path] + else + request_method = nil + end + + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + request = recognized_request_for(path, request_method) + + expected_options = expected_options.clone + extras.each_key { |key| expected_options.delete key } unless extras.nil? + + expected_options.stringify_keys! + routing_diff = expected_options.diff(request.path_parameters) + msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>", + request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) + assert_block(msg) { request.path_parameters == expected_options } + end + + # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. + # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in + # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures. + # + # The +defaults+ parameter is unused. + # + # ==== Examples + # # Asserts that the default action is generated for a route with no action + # assert_generates "/items", :controller => "items", :action => "index" + # + # # Tests that the list action is properly routed + # assert_generates "/items/list", :controller => "items", :action => "list" + # + # # Tests the generation of a route with a parameter + # assert_generates "/items/list/1", { :controller => "items", :action => "list", :id => "1" } + # + # # Asserts that the generated route gives us our custom route + # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } + def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) + expected_path = "/#{expected_path}" unless expected_path[0] == ?/ + # Load routes.rb if it hasn't been loaded. + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + + generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults) + found_extras = options.reject {|k, v| ! extra_keys.include? k} + + msg = build_message(message, "found extras <?>, not <?>", found_extras, extras) + assert_block(msg) { found_extras == extras } + + msg = build_message(message, "The generated path <?> did not match <?>", generated_path, + expected_path) + assert_block(msg) { expected_path == generated_path } + end + + # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates + # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+ + # and +assert_generates+ into one step. + # + # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The + # +message+ parameter allows you to specify a custom error message to display upon failure. + # + # ==== Examples + # # Assert a basic route: a controller with the default action (index) + # assert_routing '/home', :controller => 'home', :action => 'index' + # + # # Test a route generated with a specific controller, action, and parameter (id) + # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', id => 23 + # + # # Assert a basic route (controller + default action), with an error message if it fails + # assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly' + # + # # Tests a route, providing a defaults hash + # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"} + # + # # Tests a route with a HTTP method + # assert_routing { :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" } + def assert_routing(path, options, defaults={}, extras={}, message=nil) + assert_recognizes(options, path, extras, message) + + controller, default_controller = options[:controller], defaults[:controller] + if controller && controller.include?(?/) && default_controller && default_controller.include?(?/) + options[:controller] = "/#{controller}" + end + + assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message) + end + + private + # Recognizes the route for a given path. + def recognized_request_for(path, request_method = nil) + path = "/#{path}" unless path.first == '/' + + # Assume given controller + request = ActionController::TestRequest.new + request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method + request.path = path + + ActionController::Routing::Routes.recognize(request) + request + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb new file mode 100644 index 0000000000..dd75cda6b9 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -0,0 +1,632 @@ +#-- +# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) +# Under MIT and/or CC By license. +#++ + +module ActionDispatch + module Assertions + unless const_defined?(:NO_STRIP) + NO_STRIP = %w{pre script style textarea} + end + + # Adds the +assert_select+ method for use in Rails functional + # test cases, which can be used to make assertions on the response HTML of a controller + # action. You can also call +assert_select+ within another +assert_select+ to + # make assertions on elements selected by the enclosing assertion. + # + # Use +css_select+ to select elements without making an assertions, either + # from the response HTML or elements selected by the enclosing assertion. + # + # In addition to HTML responses, you can make the following assertions: + # * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations. + # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions. + # * +assert_select_email+ - Assertions on the HTML body of an e-mail. + # + # Also see HTML::Selector to learn how to use selectors. + module SelectorAssertions + # :call-seq: + # css_select(selector) => array + # css_select(element, selector) => array + # + # Select and return all matching elements. + # + # If called with a single argument, uses that argument as a selector + # to match all elements of the current page. Returns an empty array + # if no match is found. + # + # If called with two arguments, uses the first argument as the base + # element and the second argument as the selector. Attempts to match the + # base element and any of its children. Returns an empty array if no + # match is found. + # + # The selector may be a CSS selector expression (String), an expression + # with substitution values (Array) or an HTML::Selector object. + # + # ==== Examples + # # Selects all div tags + # divs = css_select("div") + # + # # Selects all paragraph tags and does something interesting + # pars = css_select("p") + # pars.each do |par| + # # Do something fun with paragraphs here... + # end + # + # # Selects all list items in unordered lists + # items = css_select("ul>li") + # + # # Selects all form tags and then all inputs inside the form + # forms = css_select("form") + # forms.each do |form| + # inputs = css_select(form, "input") + # ... + # end + # + def css_select(*args) + # See assert_select to understand what's going on here. + arg = args.shift + + if arg.is_a?(HTML::Node) + root = arg + arg = args.shift + elsif arg == nil + raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" + elsif @selected + matches = [] + + @selected.each do |selected| + subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) + subset.each do |match| + matches << match unless matches.any? { |m| m.equal?(match) } + end + end + + return matches + else + root = response_from_page_or_rjs + end + + case arg + when String + selector = HTML::Selector.new(arg, args) + when Array + selector = HTML::Selector.new(*arg) + when HTML::Selector + selector = arg + else raise ArgumentError, "Expecting a selector as the first argument" + end + + selector.select(root) + end + + # :call-seq: + # assert_select(selector, equality?, message?) + # assert_select(element, selector, equality?, message?) + # + # An assertion that selects elements and makes one or more equality tests. + # + # If the first argument is an element, selects all matching elements + # starting from (and including) that element and all its children in + # depth-first order. + # + # If no element if specified, calling +assert_select+ selects from the + # response HTML unless +assert_select+ is called from within an +assert_select+ block. + # + # When called with a block +assert_select+ passes an array of selected elements + # to the block. Calling +assert_select+ from the block, with no element specified, + # runs the assertion on the complete set of elements selected by the enclosing assertion. + # Alternatively the array may be iterated through so that +assert_select+ can be called + # separately for each element. + # + # + # ==== Example + # If the response contains two ordered lists, each with four list elements then: + # assert_select "ol" do |elements| + # elements.each do |element| + # assert_select element, "li", 4 + # end + # end + # + # will pass, as will: + # assert_select "ol" do + # assert_select "li", 8 + # end + # + # The selector may be a CSS selector expression (String), an expression + # with substitution values, or an HTML::Selector object. + # + # === Equality Tests + # + # The equality test may be one of the following: + # * <tt>true</tt> - Assertion is true if at least one element selected. + # * <tt>false</tt> - Assertion is true if no element selected. + # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least + # one element matches the string or regular expression. + # * <tt>Integer</tt> - Assertion is true if exactly that number of + # elements are selected. + # * <tt>Range</tt> - Assertion is true if the number of selected + # elements fit the range. + # If no equality test specified, the assertion is true if at least one + # element selected. + # + # To perform more than one equality tests, use a hash with the following keys: + # * <tt>:text</tt> - Narrow the selection to elements that have this text + # value (string or regexp). + # * <tt>:html</tt> - Narrow the selection to elements that have this HTML + # content (string or regexp). + # * <tt>:count</tt> - Assertion is true if the number of selected elements + # is equal to this value. + # * <tt>:minimum</tt> - Assertion is true if the number of selected + # elements is at least this value. + # * <tt>:maximum</tt> - Assertion is true if the number of selected + # elements is at most this value. + # + # If the method is called with a block, once all equality tests are + # evaluated the block is called with an array of all matched elements. + # + # ==== Examples + # + # # At least one form element + # assert_select "form" + # + # # Form element includes four input fields + # assert_select "form input", 4 + # + # # Page title is "Welcome" + # assert_select "title", "Welcome" + # + # # Page title is "Welcome" and there is only one title element + # assert_select "title", {:count=>1, :text=>"Welcome"}, + # "Wrong title or more than one title element" + # + # # Page contains no forms + # assert_select "form", false, "This page must contain no forms" + # + # # Test the content and style + # assert_select "body div.header ul.menu" + # + # # Use substitution values + # assert_select "ol>li#?", /item-\d+/ + # + # # All input fields in the form have a name + # assert_select "form input" do + # assert_select "[name=?]", /.+/ # Not empty + # end + def assert_select(*args, &block) + # Start with optional element followed by mandatory selector. + arg = args.shift + + if arg.is_a?(HTML::Node) + # First argument is a node (tag or text, but also HTML root), + # so we know what we're selecting from. + root = arg + arg = args.shift + elsif arg == nil + # This usually happens when passing a node/element that + # happens to be nil. + raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" + elsif @selected + root = HTML::Node.new(nil) + root.children.concat @selected + else + # Otherwise just operate on the response document. + root = response_from_page_or_rjs + end + + # First or second argument is the selector: string and we pass + # all remaining arguments. Array and we pass the argument. Also + # accepts selector itself. + case arg + when String + selector = HTML::Selector.new(arg, args) + when Array + selector = HTML::Selector.new(*arg) + when HTML::Selector + selector = arg + else raise ArgumentError, "Expecting a selector as the first argument" + end + + # Next argument is used for equality tests. + equals = {} + case arg = args.shift + when Hash + equals = arg + when String, Regexp + equals[:text] = arg + when Integer + equals[:count] = arg + when Range + equals[:minimum] = arg.begin + equals[:maximum] = arg.end + when FalseClass + equals[:count] = 0 + when NilClass, TrueClass + equals[:minimum] = 1 + else raise ArgumentError, "I don't understand what you're trying to match" + end + + # By default we're looking for at least one match. + if equals[:count] + equals[:minimum] = equals[:maximum] = equals[:count] + else + equals[:minimum] = 1 unless equals[:minimum] + end + + # Last argument is the message we use if the assertion fails. + message = args.shift + #- message = "No match made with selector #{selector.inspect}" unless message + if args.shift + raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" + end + + matches = selector.select(root) + # If text/html, narrow down to those elements that match it. + content_mismatch = nil + if match_with = equals[:text] + matches.delete_if do |match| + text = "" + text.force_encoding(match_with.encoding) if text.respond_to?(:force_encoding) + stack = match.children.reverse + while node = stack.pop + if node.tag? + stack.concat node.children.reverse + else + content = node.content + content.force_encoding(match_with.encoding) if content.respond_to?(:force_encoding) + text << content + end + end + text.strip! unless NO_STRIP.include?(match.name) + unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) + content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text) + true + end + end + elsif match_with = equals[:html] + matches.delete_if do |match| + html = match.children.map(&:to_s).join + html.strip! unless NO_STRIP.include?(match.name) + unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) + content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html) + true + end + end + end + # Expecting foo found bar element only if found zero, not if + # found one but expecting two. + message ||= content_mismatch if matches.empty? + # Test minimum/maximum occurrence. + min, max = equals[:minimum], equals[:maximum] + message = message || %(Expected #{count_description(min, max)} matching "#{selector.to_s}", found #{matches.size}.) + assert matches.size >= min, message if min + assert matches.size <= max, message if max + + # If a block is given call that block. Set @selected to allow + # nested assert_select, which can be nested several levels deep. + if block_given? && !matches.empty? + begin + in_scope, @selected = @selected, matches + yield matches + ensure + @selected = in_scope + end + end + + # Returns all matches elements. + matches + end + + def count_description(min, max) #:nodoc: + pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')} + + if min && max && (max != min) + "between #{min} and #{max} elements" + elsif min && !(min == 1 && max == 1) + "at least #{min} #{pluralize['element', min]}" + elsif max + "at most #{max} #{pluralize['element', max]}" + end + end + + # :call-seq: + # assert_select_rjs(id?) { |elements| ... } + # assert_select_rjs(statement, id?) { |elements| ... } + # assert_select_rjs(:insert, position, id?) { |elements| ... } + # + # Selects content from the RJS response. + # + # === Narrowing down + # + # With no arguments, asserts that one or more elements are updated or + # inserted by RJS statements. + # + # Use the +id+ argument to narrow down the assertion to only statements + # that update or insert an element with that identifier. + # + # Use the first argument to narrow down assertions to only statements + # of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>, + # <tt>:show</tt>, <tt>:hide</tt>, <tt>:toggle</tt>, <tt>:remove</tt> and + # <tt>:insert_html</tt>. + # + # Use the argument <tt>:insert</tt> followed by an insertion position to narrow + # down the assertion to only statements that insert elements in that + # position. Possible values are <tt>:top</tt>, <tt>:bottom</tt>, <tt>:before</tt> + # and <tt>:after</tt>. + # + # Using the <tt>:remove</tt> statement, you will be able to pass a block, but it will + # be ignored as there is no HTML passed for this statement. + # + # === Using blocks + # + # Without a block, +assert_select_rjs+ merely asserts that the response + # contains one or more RJS statements that replace or update content. + # + # With a block, +assert_select_rjs+ also selects all elements used in + # these statements and passes them to the block. Nested assertions are + # supported. + # + # Calling +assert_select_rjs+ with no arguments and using nested asserts + # asserts that the HTML content is returned by one or more RJS statements. + # Using +assert_select+ directly makes the same assertion on the content, + # but without distinguishing whether the content is returned in an HTML + # or JavaScript. + # + # ==== Examples + # + # # Replacing the element foo. + # # page.replace 'foo', ... + # assert_select_rjs :replace, "foo" + # + # # Replacing with the chained RJS proxy. + # # page[:foo].replace ... + # assert_select_rjs :chained_replace, 'foo' + # + # # Inserting into the element bar, top position. + # assert_select_rjs :insert, :top, "bar" + # + # # Remove the element bar + # assert_select_rjs :remove, "bar" + # + # # Changing the element foo, with an image. + # assert_select_rjs "foo" do + # assert_select "img[src=/images/logo.gif"" + # end + # + # # RJS inserts or updates a list with four items. + # assert_select_rjs do + # assert_select "ol>li", 4 + # end + # + # # The same, but shorter. + # assert_select "ol>li", 4 + def assert_select_rjs(*args, &block) + rjs_type = args.first.is_a?(Symbol) ? args.shift : nil + id = args.first.is_a?(String) ? args.shift : nil + + # If the first argument is a symbol, it's the type of RJS statement we're looking + # for (update, replace, insertion, etc). Otherwise, we're looking for just about + # any RJS statement. + if rjs_type + if rjs_type == :insert + position = args.shift + id = args.shift + insertion = "insert_#{position}".to_sym + raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion] + statement = "(#{RJS_STATEMENTS[insertion]})" + else + raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type] + statement = "(#{RJS_STATEMENTS[rjs_type]})" + end + else + statement = "#{RJS_STATEMENTS[:any]}" + end + + # Next argument we're looking for is the element identifier. If missing, we pick + # any element, otherwise we replace it in the statement. + pattern = Regexp.new( + id ? statement.gsub(RJS_ANY_ID, "\"#{id}\"") : statement + ) + + # Duplicate the body since the next step involves destroying it. + matches = nil + case rjs_type + when :remove, :show, :hide, :toggle + matches = @response.body.match(pattern) + else + @response.body.gsub(pattern) do |match| + html = unescape_rjs(match) + matches ||= [] + matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } + "" + end + end + + if matches + assert_block("") { true } # to count the assertion + if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) + begin + in_scope, @selected = @selected, matches + yield matches + ensure + @selected = in_scope + end + end + matches + else + # RJS statement not found. + case rjs_type + when :remove, :show, :hide, :toggle + flunk_message = "No RJS statement that #{rjs_type.to_s}s '#{id}' was rendered." + else + flunk_message = "No RJS statement that replaces or inserts HTML content." + end + flunk args.shift || flunk_message + end + end + + # :call-seq: + # assert_select_encoded(element?) { |elements| ... } + # + # Extracts the content of an element, treats it as encoded HTML and runs + # nested assertion on it. + # + # You typically call this method within another assertion to operate on + # all currently selected elements. You can also pass an element or array + # of elements. + # + # The content of each element is un-encoded, and wrapped in the root + # element +encoded+. It then calls the block with all un-encoded elements. + # + # ==== Examples + # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix) + # assert_select_feed :atom, 1.0 do + # # Select each entry item and then the title item + # assert_select "entry>title" do + # # Run assertions on the encoded title elements + # assert_select_encoded do + # assert_select "b" + # end + # end + # end + # + # + # # Selects all paragraph tags from within the description of an RSS feed + # assert_select_feed :rss, 2.0 do + # # Select description element of each feed item. + # assert_select "channel>item>description" do + # # Run assertions on the encoded elements. + # assert_select_encoded do + # assert_select "p" + # end + # end + # end + def assert_select_encoded(element = nil, &block) + case element + when Array + elements = element + when HTML::Node + elements = [element] + when nil + unless elements = @selected + raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" + end + else + raise ArgumentError, "Argument is optional, and may be node or array of nodes" + end + + fix_content = lambda do |node| + # Gets around a bug in the Rails 1.1 HTML parser. + node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) } + end + + selected = elements.map do |element| + text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join + root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root + css_select(root, "encoded:root", &block)[0] + end + + begin + old_selected, @selected = @selected, selected + assert_select ":root", &block + ensure + @selected = old_selected + end + end + + # :call-seq: + # assert_select_email { } + # + # Extracts the body of an email and runs nested assertions on it. + # + # You must enable deliveries for this assertion to work, use: + # ActionMailer::Base.perform_deliveries = true + # + # ==== Examples + # + # assert_select_email do + # assert_select "h1", "Email alert" + # end + # + # assert_select_email do + # items = assert_select "ol>li" + # items.each do + # # Work with items here... + # end + # end + # + def assert_select_email(&block) + deliveries = ActionMailer::Base.deliveries + assert !deliveries.empty?, "No e-mail in delivery list" + + for delivery in deliveries + for part in delivery.parts + if part["Content-Type"].to_s =~ /^text\/html\W/ + root = HTML::Document.new(part.body).root + assert_select root, ":root", &block + end + end + end + end + + protected + unless const_defined?(:RJS_STATEMENTS) + RJS_PATTERN_HTML = "\"((\\\\\"|[^\"])*)\"" + RJS_ANY_ID = "\"([^\"])*\"" + RJS_STATEMENTS = { + :chained_replace => "\\$\\(#{RJS_ANY_ID}\\)\\.replace\\(#{RJS_PATTERN_HTML}\\)", + :chained_replace_html => "\\$\\(#{RJS_ANY_ID}\\)\\.update\\(#{RJS_PATTERN_HTML}\\)", + :replace_html => "Element\\.update\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)", + :replace => "Element\\.replace\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)" + } + [:remove, :show, :hide, :toggle].each do |action| + RJS_STATEMENTS[action] = "Element\\.#{action}\\(#{RJS_ANY_ID}\\)" + end + RJS_INSERTIONS = ["top", "bottom", "before", "after"] + RJS_INSERTIONS.each do |insertion| + RJS_STATEMENTS["insert_#{insertion}".to_sym] = "Element.insert\\(#{RJS_ANY_ID}, \\{ #{insertion}: #{RJS_PATTERN_HTML} \\}\\)" + end + RJS_STATEMENTS[:insert_html] = "Element.insert\\(#{RJS_ANY_ID}, \\{ (#{RJS_INSERTIONS.join('|')}): #{RJS_PATTERN_HTML} \\}\\)" + RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") + RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ + end + + # +assert_select+ and +css_select+ call this to obtain the content in the HTML + # page, or from all the RJS statements, depending on the type of response. + def response_from_page_or_rjs() + content_type = @response.content_type + + if content_type && Mime::JS =~ content_type + body = @response.body.dup + root = HTML::Node.new(nil) + + while true + next if body.sub!(RJS_STATEMENTS[:any]) do |match| + html = unescape_rjs(match) + matches = HTML::Document.new(html).root.children.select { |n| n.tag? } + root.children.concat matches + "" + end + break + end + + root + else + html_document.root + end + end + + # Unescapes a RJS string. + def unescape_rjs(rjs_string) + # RJS encodes double quotes and line breaks. + unescaped= rjs_string.gsub('\"', '"') + unescaped.gsub!(/\\\//, '/') + unescaped.gsub!('\n', "\n") + unescaped.gsub!('\076', '>') + unescaped.gsub!('\074', '<') + # RJS encodes non-ascii characters. + unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')} + unescaped + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb new file mode 100644 index 0000000000..ef6867576e --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertions/tag.rb @@ -0,0 +1,123 @@ +module ActionDispatch + module Assertions + # Pair of assertions to testing elements in the HTML output of the response. + module TagAssertions + # Asserts that there is a tag/node/element in the body of the response + # that meets all of the given conditions. The +conditions+ parameter must + # be a hash of any of the following keys (all are optional): + # + # * <tt>:tag</tt>: the node type must match the corresponding value + # * <tt>:attributes</tt>: a hash. The node's attributes must match the + # corresponding values in the hash. + # * <tt>:parent</tt>: a hash. The node's parent must match the + # corresponding hash. + # * <tt>:child</tt>: a hash. At least one of the node's immediate children + # must meet the criteria described by the hash. + # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must + # meet the criteria described by the hash. + # * <tt>:descendant</tt>: a hash. At least one of the node's descendants + # must meet the criteria described by the hash. + # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must + # meet the criteria described by the hash. + # * <tt>:after</tt>: a hash. The node must be after any sibling meeting + # the criteria described by the hash, and at least one sibling must match. + # * <tt>:before</tt>: a hash. The node must be before any sibling meeting + # the criteria described by the hash, and at least one sibling must match. + # * <tt>:children</tt>: a hash, for counting children of a node. Accepts + # the keys: + # * <tt>:count</tt>: either a number or a range which must equal (or + # include) the number of children that match. + # * <tt>:less_than</tt>: the number of matching children must be less + # than this number. + # * <tt>:greater_than</tt>: the number of matching children must be + # greater than this number. + # * <tt>:only</tt>: another hash consisting of the keys to use + # to match on the children, and only matching children will be + # counted. + # * <tt>:content</tt>: the textual content of the node must match the + # given value. This will not match HTML tags in the body of a + # tag--only text. + # + # Conditions are matched using the following algorithm: + # + # * if the condition is a string, it must be a substring of the value. + # * if the condition is a regexp, it must match the value. + # * if the condition is a number, the value must match number.to_s. + # * if the condition is +true+, the value must not be +nil+. + # * if the condition is +false+ or +nil+, the value must be +nil+. + # + # === Examples + # + # # Assert that there is a "span" tag + # assert_tag :tag => "span" + # + # # Assert that there is a "span" tag with id="x" + # assert_tag :tag => "span", :attributes => { :id => "x" } + # + # # Assert that there is a "span" tag using the short-hand + # assert_tag :span + # + # # Assert that there is a "span" tag with id="x" using the short-hand + # assert_tag :span, :attributes => { :id => "x" } + # + # # Assert that there is a "span" inside of a "div" + # assert_tag :tag => "span", :parent => { :tag => "div" } + # + # # Assert that there is a "span" somewhere inside a table + # assert_tag :tag => "span", :ancestor => { :tag => "table" } + # + # # Assert that there is a "span" with at least one "em" child + # assert_tag :tag => "span", :child => { :tag => "em" } + # + # # Assert that there is a "span" containing a (possibly nested) + # # "strong" tag. + # assert_tag :tag => "span", :descendant => { :tag => "strong" } + # + # # Assert that there is a "span" containing between 2 and 4 "em" tags + # # as immediate children + # assert_tag :tag => "span", + # :children => { :count => 2..4, :only => { :tag => "em" } } + # + # # Get funky: assert that there is a "div", with an "ul" ancestor + # # and an "li" parent (with "class" = "enum"), and containing a + # # "span" descendant that contains text matching /hello world/ + # assert_tag :tag => "div", + # :ancestor => { :tag => "ul" }, + # :parent => { :tag => "li", + # :attributes => { :class => "enum" } }, + # :descendant => { :tag => "span", + # :child => /hello world/ } + # + # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work + # with well-formed XHTML. They recognize a few tags as implicitly self-closing + # (like br and hr and such) but will not work correctly with tags + # that allow optional closing tags (p, li, td). <em>You must explicitly + # close all of your tags to use these assertions.</em> + def assert_tag(*opts) + opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first + tag = find_tag(opts) + assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}" + end + + # Identical to +assert_tag+, but asserts that a matching tag does _not_ + # exist. (See +assert_tag+ for a full discussion of the syntax.) + # + # === Examples + # # Assert that there is not a "div" containing a "p" + # assert_no_tag :tag => "div", :descendant => { :tag => "p" } + # + # # Assert that an unordered list is empty + # assert_no_tag :tag => "ul", :descendant => { :tag => "li" } + # + # # Assert that there is not a "p" tag with between 1 to 3 "img" tags + # # as immediate children + # assert_no_tag :tag => "p", + # :children => { :count => 1..3, :only => { :tag => "img" } } + def assert_no_tag(*opts) + opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first + tag = find_tag(opts) + assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}" + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb new file mode 100644 index 0000000000..20288aa7a5 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -0,0 +1,83 @@ +module ActionDispatch + class TestRequest < Request + DEFAULT_ENV = Rack::MockRequest.env_for('/') + + def self.new(env = {}) + super + end + + def initialize(env = {}) + super(DEFAULT_ENV.merge(env)) + + self.host = 'test.host' + self.remote_addr = '0.0.0.0' + self.user_agent = 'Rails Testing' + end + + def env + write_cookies! + delete_nil_values! + super + end + + def request_method=(method) + @env['REQUEST_METHOD'] = method.to_s.upcase + end + + def host=(host) + @env['HTTP_HOST'] = host + end + + def port=(number) + @env['SERVER_PORT'] = number.to_i + end + + def request_uri=(uri) + @env['REQUEST_URI'] = uri + end + + def path=(path) + @env['PATH_INFO'] = path + end + + def action=(action_name) + path_parameters["action"] = action_name.to_s + end + + def if_modified_since=(last_modified) + @env['HTTP_IF_MODIFIED_SINCE'] = last_modified + end + + def if_none_match=(etag) + @env['HTTP_IF_NONE_MATCH'] = etag + end + + def remote_addr=(addr) + @env['REMOTE_ADDR'] = addr + end + + def user_agent=(user_agent) + @env['HTTP_USER_AGENT'] = user_agent + end + + def accept=(mime_types) + @env.delete('action_dispatch.request.accepts') + @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") + end + + def cookies + @cookies ||= super + end + + private + def write_cookies! + unless @cookies.blank? + @env['HTTP_COOKIE'] = @cookies.map { |name, value| "#{name}=#{value};" }.join(' ') + end + end + + def delete_nil_values! + @env.delete_if { |k, v| v.nil? } + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb new file mode 100644 index 0000000000..c35982e075 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -0,0 +1,131 @@ +module ActionDispatch + # Integration test methods such as ActionController::Integration::Session#get + # and ActionController::Integration::Session#post return objects of class + # TestResponse, which represent the HTTP response results of the requested + # controller actions. + # + # See Response for more information on controller response objects. + class TestResponse < Response + def self.from_response(response) + new.tap do |resp| + resp.status = response.status + resp.headers = response.headers + resp.body = response.body + end + end + + module DeprecatedHelpers + def template + ActiveSupport::Deprecation.warn("response.template has been deprecated. Use controller.template instead", caller) + @template + end + attr_writer :template + + def session + ActiveSupport::Deprecation.warn("response.session has been deprecated. Use request.session instead", caller) + @request.session + end + + def assigns + ActiveSupport::Deprecation.warn("response.assigns has been deprecated. Use controller.assigns instead", caller) + @template.controller.assigns + end + + def layout + ActiveSupport::Deprecation.warn("response.layout has been deprecated. Use template.layout instead", caller) + @template.layout + end + + def redirect_url_match?(pattern) + ::ActiveSupport::Deprecation.warn("response.redirect_url_match? is deprecated. Use assert_match(/foo/, response.redirect_url) instead", caller) + return false if redirect_url.nil? + p = Regexp.new(pattern) if pattern.class == String + p = pattern if pattern.class == Regexp + return false if p.nil? + p.match(redirect_url) != nil + end + + # Returns the template of the file which was used to + # render this response (or nil) + def rendered + ActiveSupport::Deprecation.warn("response.rendered has been deprecated. Use tempate.rendered instead", caller) + @template.instance_variable_get(:@_rendered) + end + + # A shortcut to the flash. Returns an empty hash if no session flash exists. + def flash + ActiveSupport::Deprecation.warn("response.flash has been deprecated. Use request.flash instead", caller) + request.session['flash'] || {} + end + + # Do we have a flash? + def has_flash? + ActiveSupport::Deprecation.warn("response.has_flash? has been deprecated. Use flash.any? instead", caller) + !flash.empty? + end + + # Do we have a flash that has contents? + def has_flash_with_contents? + ActiveSupport::Deprecation.warn("response.has_flash_with_contents? has been deprecated. Use flash.any? instead", caller) + !flash.empty? + end + + # Does the specified flash object exist? + def has_flash_object?(name=nil) + ActiveSupport::Deprecation.warn("response.has_flash_object? has been deprecated. Use flash[name] instead", caller) + !flash[name].nil? + end + + # Does the specified object exist in the session? + def has_session_object?(name=nil) + ActiveSupport::Deprecation.warn("response.has_session_object? has been deprecated. Use session[name] instead", caller) + !session[name].nil? + end + + # A shortcut to the template.assigns + def template_objects + ActiveSupport::Deprecation.warn("response.template_objects has been deprecated. Use tempate.assigns instead", caller) + @template.assigns || {} + end + + # Does the specified template object exist? + def has_template_object?(name=nil) + ActiveSupport::Deprecation.warn("response.has_template_object? has been deprecated. Use tempate.assigns[name].nil? instead", caller) + !template_objects[name].nil? + end + + # Returns binary content (downloadable file), converted to a String + def binary_content + ActiveSupport::Deprecation.warn("response.binary_content has been deprecated. Use response.body instead", caller) + body + end + end + include DeprecatedHelpers + + # Was the response successful? + def success? + (200..299).include?(response_code) + end + + # Was the URL not found? + def missing? + response_code == 404 + end + + # Were we redirected? + def redirect? + (300..399).include?(response_code) + end + + # Was there a server-side error? + def error? + (500..599).include?(response_code) + end + alias_method :server_error?, :error? + + # Was there a client client? + def client_error? + (400..499).include?(response_code) + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/reloader.rb b/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/reloader.rb deleted file mode 100644 index b17d8c0926..0000000000 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/reloader.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'thread' - -module Rack - # Rack::Reloader checks on every request, but at most every +secs+ - # seconds, if a file loaded changed, and reloads it, logging to - # rack.errors. - # - # It is recommended you use ShowExceptions to catch SyntaxErrors etc. - - class Reloader - def initialize(app, secs=10) - @app = app - @secs = secs # reload every @secs seconds max - @last = Time.now - end - - def call(env) - if Time.now > @last + @secs - Thread.exclusive { - reload!(env['rack.errors']) - @last = Time.now - } - end - - @app.call(env) - end - - def reload!(stderr=$stderr) - need_reload = $LOADED_FEATURES.find_all { |loaded| - begin - if loaded =~ /\A[.\/]/ # absolute filename or 1.9 - abs = loaded - else - abs = $LOAD_PATH.map { |path| ::File.join(path, loaded) }. - find { |file| ::File.exist? file } - end - - if abs - ::File.mtime(abs) > @last - @secs rescue false - else - false - end - end - } - - need_reload.each { |l| - $LOADED_FEATURES.delete l - } - - need_reload.each { |to_load| - begin - if require to_load - stderr.puts "#{self.class}: reloaded `#{to_load}'" - end - rescue LoadError, SyntaxError => e - raise e # Possibly ShowExceptions - end - } - - stderr.flush - need_reload - end - end -end diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack.rb index 6349b95094..371d015690 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack.rb @@ -3,7 +3,8 @@ # Rack is freely distributable under the terms of an MIT-style license. # See COPYING or http://www.opensource.org/licenses/mit-license.php. -$:.unshift(File.expand_path(File.dirname(__FILE__))) +path = File.expand_path(File.dirname(__FILE__)) +$:.unshift(path) unless $:.include?(path) # The Rack main module, serving as a namespace for all core Rack @@ -14,7 +15,7 @@ $:.unshift(File.expand_path(File.dirname(__FILE__))) module Rack # The Rack protocol version number implemented. - VERSION = [0,1] + VERSION = [1,0] # Return the Rack protocol version as a dotted string. def self.version @@ -23,7 +24,7 @@ module Rack # Return the Rack release as a dotted string. def self.release - "1.0 bundled" + "1.0" end autoload :Builder, "rack/builder" diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/adapter/camping.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/adapter/camping.rb index 63bc787f54..63bc787f54 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/adapter/camping.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/adapter/camping.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/abstract/handler.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/abstract/handler.rb index 214df6299e..214df6299e 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/abstract/handler.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/abstract/handler.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/abstract/request.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/abstract/request.rb index 1d9ccec685..1d9ccec685 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/abstract/request.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/abstract/request.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/basic.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/basic.rb index 9557224648..9557224648 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/basic.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/basic.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/md5.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/md5.rb index e579dc9632..e579dc9632 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/md5.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/md5.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/nonce.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/nonce.rb index dbe109f29a..dbe109f29a 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/nonce.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/nonce.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/params.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/params.rb index 730e2efdc8..730e2efdc8 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/params.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/params.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/request.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/request.rb index a8aa3bf996..a8aa3bf996 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/digest/request.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/digest/request.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/openid.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/openid.rb index c5f6a5143e..c5f6a5143e 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/auth/openid.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/auth/openid.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/builder.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/builder.rb index 295235e56a..295235e56a 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/builder.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/builder.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/cascade.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/cascade.rb index a038aa1105..a038aa1105 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/cascade.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/cascade.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/chunked.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/chunked.rb index 280d89dd65..280d89dd65 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/chunked.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/chunked.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/commonlogger.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/commonlogger.rb index 5e68ac626d..5e68ac626d 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/commonlogger.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/commonlogger.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/conditionalget.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/conditionalget.rb index 7bec824181..046ebdb00a 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/conditionalget.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/conditionalget.rb @@ -26,6 +26,8 @@ module Rack headers = Utils::HeaderHash.new(headers) if etag_matches?(env, headers) || modified_since?(env, headers) status = 304 + headers.delete('Content-Type') + headers.delete('Content-Length') body = [] end [status, headers, body] diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/content_length.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/content_length.rb index 1e56d43853..1e56d43853 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/content_length.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/content_length.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/content_type.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/content_type.rb index 0c1e1ca3e1..0c1e1ca3e1 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/content_type.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/content_type.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/deflater.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/deflater.rb index a42b7477ae..14137a944d 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/deflater.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/deflater.rb @@ -33,17 +33,15 @@ module Rack case encoding when "gzip" + headers['Content-Encoding'] = "gzip" + headers.delete('Content-Length') mtime = headers.key?("Last-Modified") ? Time.httpdate(headers["Last-Modified"]) : Time.now - body = self.class.gzip(body, mtime) - size = Rack::Utils.bytesize(body) - headers = headers.merge("Content-Encoding" => "gzip", "Content-Length" => size.to_s) - [status, headers, [body]] + [status, headers, GzipStream.new(body, mtime)] when "deflate" - body = self.class.deflate(body) - size = Rack::Utils.bytesize(body) - headers = headers.merge("Content-Encoding" => "deflate", "Content-Length" => size.to_s) - [status, headers, [body]] + headers['Content-Encoding'] = "deflate" + headers.delete('Content-Length') + [status, headers, DeflateStream.new(body)] when "identity" [status, headers, body] when nil @@ -52,34 +50,47 @@ module Rack end end - def self.gzip(body, mtime) - io = StringIO.new - gzip = Zlib::GzipWriter.new(io) - gzip.mtime = mtime + class GzipStream + def initialize(body, mtime) + @body = body + @mtime = mtime + end - # TODO: Add streaming - body.each { |part| gzip << part } + def each(&block) + @writer = block + gzip =::Zlib::GzipWriter.new(self) + gzip.mtime = @mtime + @body.each { |part| gzip << part } + @body.close if @body.respond_to?(:close) + gzip.close + @writer = nil + end - gzip.close - return io.string + def write(data) + @writer.call(data) + end end - DEFLATE_ARGS = [ - Zlib::DEFAULT_COMPRESSION, - # drop the zlib header which causes both Safari and IE to choke - -Zlib::MAX_WBITS, - Zlib::DEF_MEM_LEVEL, - Zlib::DEFAULT_STRATEGY - ] + class DeflateStream + DEFLATE_ARGS = [ + Zlib::DEFAULT_COMPRESSION, + # drop the zlib header which causes both Safari and IE to choke + -Zlib::MAX_WBITS, + Zlib::DEF_MEM_LEVEL, + Zlib::DEFAULT_STRATEGY + ] - # Loosely based on Mongrel's Deflate handler - def self.deflate(body) - deflater = Zlib::Deflate.new(*DEFLATE_ARGS) - - # TODO: Add streaming - body.each { |part| deflater << part } + def initialize(body) + @body = body + end - return deflater.finish + def each + deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS) + @body.each { |part| yield deflater.deflate(part) } + @body.close if @body.respond_to?(:close) + yield deflater.finish + nil + end end end end diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/directory.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/directory.rb index acdd3029d3..acdd3029d3 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/directory.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/directory.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/file.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/file.rb index fe62bd6b86..fe62bd6b86 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/file.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/file.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler.rb index 1018af64c7..5624a1e79d 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler.rb @@ -10,16 +10,37 @@ module Rack module Handler def self.get(server) return unless server + server = server.to_s if klass = @handlers[server] obj = Object klass.split("::").each { |x| obj = obj.const_get(x) } obj else - Rack::Handler.const_get(server.capitalize) + try_require('rack/handler', server) + const_get(server) end end + # Transforms server-name constants to their canonical form as filenames, + # then tries to require them but silences the LoadError if not found + # + # Naming convention: + # + # Foo # => 'foo' + # FooBar # => 'foo_bar.rb' + # FooBAR # => 'foobar.rb' + # FOObar # => 'foobar.rb' + # FOOBAR # => 'foobar.rb' + # FooBarBaz # => 'foo_bar_baz.rb' + def self.try_require(prefix, const_name) + file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }. + gsub(/[A-Z]+[^A-Z]/, '_\&').downcase + + require(::File.join(prefix, file)) + rescue LoadError + end + def self.register(server, klass) @handlers ||= {} @handlers[server] = klass diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/cgi.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/cgi.rb index e38156c7f0..f45f3d735a 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/cgi.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/cgi.rb @@ -15,7 +15,7 @@ module Rack env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" - env.update({"rack.version" => [0,1], + env.update({"rack.version" => [1,0], "rack.input" => $stdin, "rack.errors" => $stderr, diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/evented_mongrel.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/evented_mongrel.rb index 0f5cbf7293..0f5cbf7293 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/evented_mongrel.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/evented_mongrel.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/fastcgi.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/fastcgi.rb index 6324c7d274..11e1fcaa74 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/fastcgi.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/fastcgi.rb @@ -1,6 +1,17 @@ require 'fcgi' require 'socket' require 'rack/content_length' +require 'rack/rewindable_input' + +class FCGI::Stream + alias _rack_read_without_buffer read + + def read(n, buffer=nil) + buf = _rack_read_without_buffer n + buffer.replace(buf.to_s) if buffer + buf + end +end module Rack module Handler @@ -13,34 +24,18 @@ module Rack } end - module ProperStream # :nodoc: - def each # This is missing by default. - while line = gets - yield line - end - end - - def read(*args) - if args.empty? - super || "" # Empty string on EOF. - else - super - end - end - end - def self.serve(request, app) app = Rack::ContentLength.new(app) env = request.env env.delete "HTTP_CONTENT_LENGTH" - request.in.extend ProperStream - env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + + rack_input = RewindableInput.new(request.in) - env.update({"rack.version" => [0,1], - "rack.input" => request.in, + env.update({"rack.version" => [1,0], + "rack.input" => rack_input, "rack.errors" => request.err, "rack.multithread" => false, @@ -57,12 +52,16 @@ module Rack env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == "" env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == "" - status, headers, body = app.call(env) begin - send_headers request.out, status, headers - send_body request.out, body + status, headers, body = app.call(env) + begin + send_headers request.out, status, headers + send_body request.out, body + ensure + body.close if body.respond_to? :close + end ensure - body.close if body.respond_to? :close + rack_input.close request.finish end end diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/lsws.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/lsws.rb index c65ba3ec8e..7231336d7b 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/lsws.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/lsws.rb @@ -15,7 +15,7 @@ module Rack env = ENV.to_hash env.delete "HTTP_CONTENT_LENGTH" env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" - env.update({"rack.version" => [0,1], + env.update({"rack.version" => [1,0], "rack.input" => StringIO.new($stdin.read.to_s), "rack.errors" => $stderr, "rack.multithread" => false, diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/mongrel.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/mongrel.rb index f0c0d58330..3a5ef32d4b 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/mongrel.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/mongrel.rb @@ -45,7 +45,7 @@ module Rack env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" - env.update({"rack.version" => [0,1], + env.update({"rack.version" => [1,0], "rack.input" => request.body || StringIO.new(""), "rack.errors" => $stderr, diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/scgi.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/scgi.rb index 9495c66374..6c4932df95 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/scgi.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/scgi.rb @@ -32,7 +32,7 @@ module Rack env["PATH_INFO"] = env["REQUEST_PATH"] env["QUERY_STRING"] ||= "" env["SCRIPT_NAME"] = "" - env.update({"rack.version" => [0,1], + env.update({"rack.version" => [1,0], "rack.input" => StringIO.new(input_body), "rack.errors" => $stderr, diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/swiftiplied_mongrel.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/swiftiplied_mongrel.rb index 4bafd0b953..4bafd0b953 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/swiftiplied_mongrel.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/swiftiplied_mongrel.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/thin.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/thin.rb index 3d4fedff75..3d4fedff75 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/thin.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/thin.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/webrick.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/webrick.rb index 829e7d6bf8..2bdc83a9ff 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/handler/webrick.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/handler/webrick.rb @@ -22,7 +22,7 @@ module Rack env = req.meta_vars env.delete_if { |k, v| v.nil? } - env.update({"rack.version" => [0,1], + env.update({"rack.version" => [1,0], "rack.input" => StringIO.new(req.body.to_s), "rack.errors" => $stderr, diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/head.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/head.rb index deab822a99..deab822a99 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/head.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/head.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/lint.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/lint.rb index 44a33ce36e..bf2e9787a1 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/lint.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/lint.rb @@ -88,9 +88,9 @@ module Rack ## within the application. This may be an ## empty string, if the request URL targets ## the application root and does not have a - ## trailing slash. This information should be - ## decoded by the server if it comes from a - ## URL. + ## trailing slash. This value may be + ## percent-encoded when I originating from + ## a URL. ## <tt>QUERY_STRING</tt>:: The portion of the request URL that ## follows the <tt>?</tt>, if any. May be @@ -111,19 +111,48 @@ module Rack ## In addition to this, the Rack environment must include these ## Rack-specific variables: - ## <tt>rack.version</tt>:: The Array [0,1], representing this version of Rack. + ## <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack. ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL. ## <tt>rack.input</tt>:: See below, the input stream. ## <tt>rack.errors</tt>:: See below, the error stream. ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). + ## + + ## Additional environment specifications have approved to + ## standardized middleware APIs. None of these are required to + ## be implemented by the server. + + ## <tt>rack.session</tt>:: A hash like interface for storing request session data. + ## The store must implement: + if session = env['rack.session'] + ## store(key, value) (aliased as []=); + assert("session #{session.inspect} must respond to store and []=") { + session.respond_to?(:store) && session.respond_to?(:[]=) + } + + ## fetch(key, default = nil) (aliased as []); + assert("session #{session.inspect} must respond to fetch and []") { + session.respond_to?(:fetch) && session.respond_to?(:[]) + } + + ## delete(key); + assert("session #{session.inspect} must respond to delete") { + session.respond_to?(:delete) + } + + ## clear; + assert("session #{session.inspect} must respond to clear") { + session.respond_to?(:clear) + } + end ## The server or the application can store their own data in the ## environment, too. The keys must contain at least one dot, ## and should be prefixed uniquely. The prefix <tt>rack.</tt> - ## is reserved for use with the Rack core distribution and must - ## not be used otherwise. + ## is reserved for use with the Rack core distribution and other + ## accepted specifications and must not be used otherwise. ## %w[REQUEST_METHOD SERVER_NAME SERVER_PORT @@ -202,9 +231,12 @@ module Rack end ## === The Input Stream + ## + ## The input stream is an IO-like object which contains the raw HTTP + ## POST data. If it is a file then it must be opened in binary mode. def check_input(input) - ## The input stream must respond to +gets+, +each+ and +read+. - [:gets, :each, :read].each { |method| + ## The input stream must respond to +gets+, +each+, +read+ and +rewind+. + [:gets, :each, :read, :rewind].each { |method| assert("rack.input #{input} does not respond to ##{method}") { input.respond_to? method } @@ -222,10 +254,6 @@ module Rack @input.size end - def rewind - @input.rewind - end - ## * +gets+ must be called without arguments and return a string, ## or +nil+ on EOF. def gets(*args) @@ -237,21 +265,44 @@ module Rack v end - ## * +read+ must be called without or with one integer argument - ## and return a string, or +nil+ on EOF. + ## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>. + ## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must + ## be a String and may not be nil. If +length+ is given and not nil, then this method + ## reads at most +length+ bytes from the input stream. If +length+ is not given or nil, + ## then this method reads all data until EOF. + ## When EOF is reached, this method returns nil if +length+ is given and not nil, or "" + ## if +length+ is not given or is nil. + ## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a + ## newly created String object. def read(*args) assert("rack.input#read called with too many arguments") { - args.size <= 1 + args.size <= 2 } - if args.size == 1 - assert("rack.input#read called with non-integer argument") { - args.first.kind_of? Integer + if args.size >= 1 + assert("rack.input#read called with non-integer and non-nil length") { + args.first.kind_of?(Integer) || args.first.nil? + } + assert("rack.input#read called with a negative length") { + args.first.nil? || args.first >= 0 } end + if args.size >= 2 + assert("rack.input#read called with non-String buffer") { + args[1].kind_of?(String) + } + end + v = @input.read(*args) - assert("rack.input#read didn't return a String") { + + assert("rack.input#read didn't return nil or a String") { v.nil? or v.instance_of? String } + if args[0].nil? + assert("rack.input#read(nil) returned nil on EOF") { + !v.nil? + } + end + v end @@ -265,6 +316,23 @@ module Rack yield line } end + + ## * +rewind+ must be called without arguments. It rewinds the input + ## stream back to the beginning. It must not raise Errno::ESPIPE: + ## that is, it may not be a pipe or a socket. Therefore, handler + ## developers must buffer the input data into some rewindable object + ## if the underlying input stream is not rewindable. + def rewind(*args) + assert("rack.input#rewind called with arguments") { args.size == 0 } + assert("rack.input#rewind raised Errno::ESPIPE") { + begin + @input.rewind + true + rescue Errno::ESPIPE + false + end + } + end ## * +close+ must never be called on the input stream. def close(*args) @@ -316,13 +384,14 @@ module Rack ## === The Status def check_status(status) - ## The status, if parsed as integer (+to_i+), must be greater than or equal to 100. + ## This is an HTTP status. When parsed as integer (+to_i+), it must be + ## greater than or equal to 100. assert("Status must be >=100 seen as integer") { status.to_i >= 100 } end ## === The Headers def check_headers(header) - ## The header must respond to each, and yield values of key and value. + ## The header must respond to +each+, and yield values of key and value. assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") { header.respond_to? :each } @@ -344,7 +413,8 @@ module Rack ## The values of the header must be Strings, assert("a header value must be a String, but the value of " + "'#{key}' is a #{value.class}") { value.kind_of? String } - ## consisting of lines (for multiple header values) seperated by "\n". + ## consisting of lines (for multiple header values, e.g. multiple + ## <tt>Set-Cookie</tt> values) seperated by "\n". value.split("\n").each { |item| ## The lines must not contain characters below 037. assert("invalid header value #{key}: #{item.inspect}") { @@ -416,7 +486,7 @@ module Rack ## === The Body def each @closed = false - ## The Body must respond to #each + ## The Body must respond to +each+ @body.each { |part| ## and must only yield String values. assert("Body yielded non-string value #{part.inspect}") { @@ -425,14 +495,19 @@ module Rack yield part } ## - ## If the Body responds to #close, it will be called after iteration. + ## The Body itself should not be an instance of String, as this will + ## break in Ruby 1.9. + ## + ## If the Body responds to +close+, it will be called after iteration. # XXX howto: assert("Body has not been closed") { @closed } ## - ## If the Body responds to #to_path, it must return a String + ## If the Body responds to +to_path+, it must return a String ## identifying the location of a file whose contents are identical - ## to that produced by calling #each. + ## to that produced by calling +each+; this may be used by the + ## server as an alternative, possibly more efficient way to + ## transport the response. if @body.respond_to?(:to_path) assert("The file identified by body.to_path does not exist") { diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/lobster.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/lobster.rb index f63f419a49..f63f419a49 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/lobster.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/lobster.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/lock.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/lock.rb index 93238528c4..93238528c4 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/lock.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/lock.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/methodoverride.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/methodoverride.rb index 0eed29f471..0eed29f471 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/methodoverride.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/methodoverride.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/mime.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/mime.rb index 5a6a73a97b..5a6a73a97b 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/mime.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/mime.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/mock.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/mock.rb index 70852da3db..fdefb0340a 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/mock.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/mock.rb @@ -40,7 +40,7 @@ module Rack end DEFAULT_ENV = { - "rack.version" => [0,1], + "rack.version" => [1,0], "rack.input" => StringIO.new, "rack.errors" => StringIO.new, "rack.multithread" => true, @@ -73,14 +73,17 @@ module Rack # Return the Rack environment used for a request to +uri+. def self.env_for(uri="", opts={}) uri = URI(uri) + uri.path = "/#{uri.path}" unless uri.path[0] == ?/ + env = DEFAULT_ENV.dup - env["REQUEST_METHOD"] = opts[:method] || "GET" + env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET" env["SERVER_NAME"] = uri.host || "example.org" env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80" env["QUERY_STRING"] = uri.query.to_s env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path env["rack.url_scheme"] = uri.scheme || "http" + env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off" env["SCRIPT_NAME"] = opts[:script_name] || "" @@ -90,6 +93,27 @@ module Rack env["rack.errors"] = StringIO.new end + if params = opts[:params] + if env["REQUEST_METHOD"] == "GET" + params = Utils.parse_nested_query(params) if params.is_a?(String) + params.update(Utils.parse_nested_query(env["QUERY_STRING"])) + env["QUERY_STRING"] = Utils.build_nested_query(params) + elsif !opts.has_key?(:input) + opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + if params.is_a?(Hash) + if data = Utils::Multipart.build_multipart(params) + opts[:input] = data + opts["CONTENT_LENGTH"] ||= data.length.to_s + opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}" + else + opts[:input] = Utils.build_nested_query(params) + end + else + opts[:input] = params + end + end + end + opts[:input] ||= "" if String === opts[:input] env["rack.input"] = StringIO.new(opts[:input]) @@ -125,7 +149,7 @@ module Rack @body = "" body.each { |part| @body << part } - @errors = errors.string + @errors = errors.string if errors.respond_to?(:string) end # Status diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/recursive.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/recursive.rb index bf8b965925..bf8b965925 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/recursive.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/recursive.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/reloader.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/reloader.rb new file mode 100644 index 0000000000..aa2f060be5 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/reloader.rb @@ -0,0 +1,106 @@ +# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com +# All files in this distribution are subject to the terms of the Ruby license. + +require 'pathname' + +module Rack + + # High performant source reloader + # + # This class acts as Rack middleware. + # + # What makes it especially suited for use in a production environment is that + # any file will only be checked once and there will only be made one system + # call stat(2). + # + # Please note that this will not reload files in the background, it does so + # only when actively called. + # + # It is performing a check/reload cycle at the start of every request, but + # also respects a cool down time, during which nothing will be done. + class Reloader + def initialize(app, cooldown = 10, backend = Stat) + @app = app + @cooldown = cooldown + @last = (Time.now - cooldown) + @cache = {} + @mtimes = {} + + extend backend + end + + def call(env) + if @cooldown and Time.now > @last + @cooldown + if Thread.list.size > 1 + Thread.exclusive{ reload! } + else + reload! + end + + @last = Time.now + end + + @app.call(env) + end + + def reload!(stderr = $stderr) + rotation do |file, mtime| + previous_mtime = @mtimes[file] ||= mtime + safe_load(file, mtime, stderr) if mtime > previous_mtime + end + end + + # A safe Kernel::load, issuing the hooks depending on the results + def safe_load(file, mtime, stderr = $stderr) + load(file) + stderr.puts "#{self.class}: reloaded `#{file}'" + file + rescue LoadError, SyntaxError => ex + stderr.puts ex + ensure + @mtimes[file] = mtime + end + + module Stat + def rotation + files = [$0, *$LOADED_FEATURES].uniq + paths = ['./', *$LOAD_PATH].uniq + + files.map{|file| + next if file =~ /\.(so|bundle)$/ # cannot reload compiled files + + found, stat = figure_path(file, paths) + next unless found and stat and mtime = stat.mtime + + @cache[file] = found + + yield(found, mtime) + }.compact + end + + # Takes a relative or absolute +file+ name, a couple possible +paths+ that + # the +file+ might reside in. Returns the full path and File::Stat for the + # path. + def figure_path(file, paths) + found = @cache[file] + found = file if !found and Pathname.new(file).absolute? + found, stat = safe_stat(found) + return found, stat if found + + paths.each do |possible_path| + path = ::File.join(possible_path, file) + found, stat = safe_stat(path) + return ::File.expand_path(found), stat if found + end + end + + def safe_stat(file) + return unless file + stat = ::File.stat(file) + return file, stat if stat.file? + rescue Errno::ENOENT, Errno::ENOTDIR + @cache.delete(file) and false + end + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/request.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/request.rb index d77fa26575..0bff7af038 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/request.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/request.rb @@ -17,7 +17,7 @@ module Rack # The environment of the request. attr_reader :env - def self.new(env) + def self.new(env, *args) if self == Rack::Request env["rack.request"] ||= super else @@ -38,6 +38,8 @@ module Rack def query_string; @env["QUERY_STRING"].to_s end def content_length; @env['CONTENT_LENGTH'] end def content_type; @env['CONTENT_TYPE'] end + def session; @env['rack.session'] ||= {} end + def session_options; @env['rack.session.options'] ||= {} end # The media type (type/subtype) portion of the CONTENT_TYPE header # without any media type parameters. e.g., when CONTENT_TYPE is @@ -46,7 +48,7 @@ module Rack # For more information on the use of media types in HTTP, see: # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 def media_type - content_type && content_type.split(/\s*[;,]\s*/, 2)[0].downcase + content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase end # The media type parameters provided in CONTENT_TYPE as a Hash, or @@ -92,6 +94,14 @@ module Rack 'multipart/form-data' ] + # The set of media-types. Requests that do not indicate + # one of the media types presents in this list will not be eligible + # for param parsing like soap attachments or generic multiparts + PARSEABLE_DATA_MEDIA_TYPES = [ + 'multipart/related', + 'multipart/mixed' + ] + # Determine whether the request body contains form-data by checking # the request media_type against registered form-data media-types: # "application/x-www-form-urlencoded" and "multipart/form-data". The @@ -101,6 +111,12 @@ module Rack FORM_DATA_MEDIA_TYPES.include?(media_type) end + # Determine whether the request body contains data by checking + # the request media_type against registered parse-data media-types + def parseable_data? + PARSEABLE_DATA_MEDIA_TYPES.include?(media_type) + end + # Returns the data recieved in the query string. def GET if @env["rack.request.query_string"] == query_string @@ -119,7 +135,7 @@ module Rack def POST if @env["rack.request.form_input"].eql? @env["rack.input"] @env["rack.request.form_hash"] - elsif form_data? + elsif form_data? || parseable_data? @env["rack.request.form_input"] = @env["rack.input"] unless @env["rack.request.form_hash"] = Utils::Multipart.parse_multipart(env) @@ -131,12 +147,7 @@ module Rack @env["rack.request.form_vars"] = form_vars @env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars) - 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 + @env["rack.input"].rewind end @env["rack.request.form_hash"] else @@ -211,11 +222,13 @@ module Rack url end - + + def path + script_name + path_info + end + def fullpath - path = script_name + path_info - path << "?" << query_string unless query_string.empty? - path + query_string.empty? ? path : "#{path}?#{query_string}" end def accept_encoding diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/response.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/response.rb index caf60d5b19..28b4d8302f 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/response.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/response.rb @@ -95,6 +95,10 @@ module Rack :expires => Time.at(0) }.merge(value)) end + def redirect(target, status=302) + self.status = status + self["Location"] = target + end def finish(&block) @block = block @@ -120,7 +124,7 @@ module Rack # def write(str) s = str.to_s - @length += s.size + @length += Rack::Utils.bytesize(s) @writer.call s header["Content-Length"] = @length.to_s diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/rewindable_input.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/rewindable_input.rb new file mode 100644 index 0000000000..9e9b21ff99 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/rewindable_input.rb @@ -0,0 +1,98 @@ +require 'tempfile' + +module Rack + # Class which can make any IO object rewindable, including non-rewindable ones. It does + # this by buffering the data into a tempfile, which is rewindable. + # + # rack.input is required to be rewindable, so if your input stream IO is non-rewindable + # by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class + # to easily make it rewindable. + # + # Don't forget to call #close when you're done. This frees up temporary resources that + # RewindableInput uses, though it does *not* close the original IO object. + class RewindableInput + def initialize(io) + @io = io + @rewindable_io = nil + @unlinked = false + end + + def gets + make_rewindable unless @rewindable_io + @rewindable_io.gets + end + + def read(*args) + make_rewindable unless @rewindable_io + @rewindable_io.read(*args) + end + + def each(&block) + make_rewindable unless @rewindable_io + @rewindable_io.each(&block) + end + + def rewind + make_rewindable unless @rewindable_io + @rewindable_io.rewind + end + + # Closes this RewindableInput object without closing the originally + # wrapped IO oject. Cleans up any temporary resources that this RewindableInput + # has created. + # + # This method may be called multiple times. It does nothing on subsequent calls. + def close + if @rewindable_io + if @unlinked + @rewindable_io.close + else + @rewindable_io.close! + end + @rewindable_io = nil + end + end + + private + + # Ruby's Tempfile class has a bug. Subclass it and fix it. + class Tempfile < ::Tempfile + def _close + @tmpfile.close if @tmpfile + @data[1] = nil if @data + @tmpfile = nil + end + end + + def make_rewindable + # Buffer all data into a tempfile. Since this tempfile is private to this + # RewindableInput object, we chmod it so that nobody else can read or write + # it. On POSIX filesystems we also unlink the file so that it doesn't + # even have a file entry on the filesystem anymore, though we can still + # access it because we have the file handle open. + @rewindable_io = Tempfile.new('RackRewindableInput') + @rewindable_io.chmod(0000) + if filesystem_has_posix_semantics? + @rewindable_io.unlink + @unlinked = true + end + + buffer = "" + while @io.read(1024 * 4, buffer) + entire_buffer_written_out = false + while !entire_buffer_written_out + written = @rewindable_io.write(buffer) + entire_buffer_written_out = written == buffer.size + if !entire_buffer_written_out + buffer.slice!(0 .. written - 1) + end + end + end + @rewindable_io.rewind + end + + def filesystem_has_posix_semantics? + RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/ + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/abstract/id.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/abstract/id.rb index 218144c17f..218144c17f 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/abstract/id.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/abstract/id.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/cookie.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/cookie.rb index eace9bd0c6..eace9bd0c6 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/cookie.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/cookie.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/memcache.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/memcache.rb index 4a65cbf35d..4a65cbf35d 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/memcache.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/memcache.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/pool.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/pool.rb index f6f87408bb..f6f87408bb 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/session/pool.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/session/pool.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/showexceptions.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/showexceptions.rb index 697bc41fdb..697bc41fdb 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/showexceptions.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/showexceptions.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/showstatus.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/showstatus.rb index 28258c7c89..28258c7c89 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/showstatus.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/showstatus.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/static.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/static.rb index 168e8f83b2..168e8f83b2 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/static.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/static.rb diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/urlmap.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/urlmap.rb index 0ff32df181..fcf6616c58 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/urlmap.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/urlmap.rb @@ -30,7 +30,7 @@ module Rack location = location.chomp('/') [host, location, app] - }.sort_by { |(h, l, a)| [-l.size, h.to_s.size] } # Longest path first + }.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first end def call(env) diff --git a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/utils.rb b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/utils.rb index 0a61bce707..42e2e698f4 100644 --- a/actionpack/lib/action_dispatch/vendor/rack-1.0/rack/utils.rb +++ b/actionpack/lib/action_dispatch/vendor/rack-1.1.pre/rack/utils.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'set' require 'tempfile' @@ -63,7 +65,7 @@ module Rack module_function :parse_nested_query def normalize_params(params, name, v = nil) - name =~ %r([\[\]]*([^\[\]]+)\]*) + name =~ %r(\A[\[\]]*([^\[\]]+)\]*) k = $1 || '' after = $' || '' @@ -73,12 +75,12 @@ module Rack params[k] = v elsif after == "[]" params[k] ||= [] - raise TypeError unless params[k].is_a?(Array) + raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) params[k] << v elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$) child_key = $1 params[k] ||= [] - raise TypeError unless params[k].is_a?(Array) + raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key) normalize_params(params[k].last, child_key, v) else @@ -86,6 +88,7 @@ module Rack end else params[k] ||= {} + raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash) params[k] = normalize_params(params[k], after, v) end @@ -104,6 +107,25 @@ module Rack end module_function :build_query + def build_nested_query(value, prefix = nil) + case value + when Array + value.map { |v| + build_nested_query(v, "#{prefix}[]") + }.join("&") + when Hash + value.map { |k, v| + build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) + }.join("&") + when String + raise ArgumentError, "value must be a Hash" if prefix.nil? + "#{prefix}=#{escape(value)}" + else + prefix + end + end + module_function :build_nested_query + # Escape ampersands, brackets and quotes to their HTML/XML entities. def escape_html(string) string.to_s.gsub("&", "&"). @@ -288,11 +310,39 @@ module Rack # Usually, Rack::Request#POST takes care of calling this. module Multipart + class UploadedFile + # The filename, *not* including the path, of the "uploaded" file + attr_reader :original_filename + + # The content type of the "uploaded" file + attr_accessor :content_type + + def initialize(path, content_type = "text/plain", binary = false) + raise "#{path} file does not exist" unless ::File.exist?(path) + @content_type = content_type + @original_filename = ::File.basename(path) + @tempfile = Tempfile.new(@original_filename) + @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding) + @tempfile.binmode if binary + FileUtils.copy_file(path, @tempfile.path) + end + + def path + @tempfile.path + end + alias_method :local_path, :path + + def method_missing(method_name, *args, &block) #:nodoc: + @tempfile.__send__(method_name, *args, &block) + end + end + EOL = "\r\n" + MULTIPART_BOUNDARY = "AaB03x" def self.parse_multipart(env) unless env['CONTENT_TYPE'] =~ - %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n + %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n nil else boundary = "--#{$1}" @@ -301,13 +351,16 @@ module Rack buf = "" content_length = env['CONTENT_LENGTH'].to_i input = env['rack.input'] + input.rewind - boundary_size = boundary.size + EOL.size + boundary_size = Utils.bytesize(boundary) + EOL.size bufsize = 16384 content_length -= boundary_size - status = input.read(boundary_size) + read_buffer = '' + + status = input.read(boundary_size, read_buffer) raise EOFError, "bad content body" unless status == boundary + EOL rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n @@ -318,15 +371,15 @@ module Rack filename = content_type = name = nil until head && buf =~ rx - if !head && i = buf.index("\r\n\r\n") + if !head && i = buf.index(EOL+EOL) head = buf.slice!(0, i+2) # First \r\n buf.slice!(0, 2) # Second \r\n filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1] - content_type = head[/Content-Type: (.*)\r\n/ni, 1] - name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1] + content_type = head[/Content-Type: (.*)#{EOL}/ni, 1] + name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1] - if filename + if content_type || filename body = Tempfile.new("RackMultipart") body.binmode if body.respond_to?(:binmode) end @@ -339,7 +392,7 @@ module Rack body << buf.slice!(0, buf.size - (boundary_size+4)) end - c = input.read(bufsize < content_length ? bufsize : content_length) + c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer) raise EOFError, "bad content body" if c.nil? || c.empty? buf << c content_length -= c.size @@ -368,6 +421,12 @@ module Rack data = {:filename => filename, :type => content_type, :name => name, :tempfile => body, :head => head} + elsif !filename && content_type + body.rewind + + # Generic multipart cases, not coming from a form + data = {:type => content_type, + :name => name, :tempfile => body, :head => head} else data = body end @@ -377,16 +436,81 @@ module Rack break if buf.empty? || content_length == -1 } - begin - input.rewind if 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 + input.rewind params end end + + def self.build_multipart(params, first = true) + if first + unless params.is_a?(Hash) + raise ArgumentError, "value must be a Hash" + end + + multipart = false + query = lambda { |value| + case value + when Array + value.each(&query) + when Hash + value.values.each(&query) + when UploadedFile + multipart = true + end + } + params.values.each(&query) + return nil unless multipart + end + + flattened_params = Hash.new + + params.each do |key, value| + k = first ? key.to_s : "[#{key}]" + + case value + when Array + value.map { |v| + build_multipart(v, false).each { |subkey, subvalue| + flattened_params["#{k}[]#{subkey}"] = subvalue + } + } + when Hash + build_multipart(value, false).each { |subkey, subvalue| + flattened_params[k + subkey] = subvalue + } + else + flattened_params[k] = value + end + end + + if first + flattened_params.map { |name, file| + if file.respond_to?(:original_filename) + ::File.open(file.path, "rb") do |f| + f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) +<<-EOF +--#{MULTIPART_BOUNDARY}\r +Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r +Content-Type: #{file.content_type}\r +Content-Length: #{::File.stat(file.path).size}\r +\r +#{f.read}\r +EOF + end + else +<<-EOF +--#{MULTIPART_BOUNDARY}\r +Content-Disposition: form-data; name="#{name}"\r +\r +#{file}\r +EOF + end + }.join + "--#{MULTIPART_BOUNDARY}--\r" + else + flattened_params + end + end end end end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/mock_session.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/mock_session.rb new file mode 100644 index 0000000000..eba6226538 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/mock_session.rb @@ -0,0 +1,50 @@ +module Rack + + class MockSession + attr_writer :cookie_jar + attr_reader :last_response + + def initialize(app, default_host = Rack::Test::DEFAULT_HOST) + @app = app + @default_host = default_host + end + + def clear_cookies + @cookie_jar = Rack::Test::CookieJar.new([], @default_host) + end + + def set_cookie(cookie, uri = nil) + cookie_jar.merge(cookie, uri) + end + + def request(uri, env) + env["HTTP_COOKIE"] ||= cookie_jar.for(uri) + @last_request = Rack::Request.new(env) + status, headers, body = @app.call(@last_request.env) + @last_response = MockResponse.new(status, headers, body, env["rack.errors"].flush) + cookie_jar.merge(last_response.headers["Set-Cookie"], uri) + + @last_response + end + + # Return the last request issued in the session. Raises an error if no + # requests have been sent yet. + def last_request + raise Rack::Test::Error.new("No request yet. Request a page first.") unless @last_request + @last_request + end + + # Return the last response received in the session. Raises an error if + # no requests have been sent yet. + def last_response + raise Rack::Test::Error.new("No response yet. Request a page first.") unless @last_response + @last_response + end + + def cookie_jar + @cookie_jar ||= Rack::Test::CookieJar.new([], @default_host) + end + + end + +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/test.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test.rb new file mode 100644 index 0000000000..70384b1d76 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test.rb @@ -0,0 +1,239 @@ +unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__) + "/..")) + $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/..")) +end + +require "uri" +require "rack" +require "rack/mock_session" +require "rack/test/cookie_jar" +require "rack/test/mock_digest_request" +require "rack/test/utils" +require "rack/test/methods" +require "rack/test/uploaded_file" + +module Rack + module Test + + VERSION = "0.3.0" + + DEFAULT_HOST = "example.org" + MULTIPART_BOUNDARY = "----------XnJLe9ZIbbGUYtzPQJ16u1" + + # The common base class for exceptions raised by Rack::Test + class Error < StandardError; end + + class Session + extend Forwardable + include Rack::Test::Utils + + def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request + + # Initialize a new session for the given Rack app + def initialize(app, default_host = DEFAULT_HOST) + @headers = {} + @default_host = default_host + @rack_mock_session = Rack::MockSession.new(app, default_host) + end + + # Issue a GET request for the given URI with the given params and Rack + # environment. Stores the issues request object in #last_request and + # the app's response in #last_response. Yield #last_response to a block + # if given. + # + # Example: + # get "/" + def get(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "GET", :params => params)) + process_request(uri, env, &block) + end + + # Issue a POST request for the given URI. See #get + # + # Example: + # post "/signup", "name" => "Bryan" + def post(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "POST", :params => params)) + process_request(uri, env, &block) + end + + # Issue a PUT request for the given URI. See #get + # + # Example: + # put "/" + def put(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "PUT", :params => params)) + process_request(uri, env, &block) + end + + # Issue a DELETE request for the given URI. See #get + # + # Example: + # delete "/" + def delete(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "DELETE", :params => params)) + process_request(uri, env, &block) + end + + # Issue a HEAD request for the given URI. See #get + # + # Example: + # head "/" + def head(uri, params = {}, env = {}, &block) + env = env_for(uri, env.merge(:method => "HEAD", :params => params)) + process_request(uri, env, &block) + end + + # Issue a request to the Rack app for the given URI and optional Rack + # environment. Stores the issues request object in #last_request and + # the app's response in #last_response. Yield #last_response to a block + # if given. + # + # Example: + # request "/" + def request(uri, env = {}, &block) + env = env_for(uri, env) + process_request(uri, env, &block) + end + + # Set a header to be included on all subsequent requests through the + # session. Use a value of nil to remove a previously configured header. + # + # Example: + # header "User-Agent", "Firefox" + def header(name, value) + if value.nil? + @headers.delete(name) + else + @headers[name] = value + end + end + + # Set the username and password for HTTP Basic authorization, to be + # included in subsequent requests in the HTTP_AUTHORIZATION header. + # + # Example: + # basic_authorize "bryan", "secret" + def basic_authorize(username, password) + encoded_login = ["#{username}:#{password}"].pack("m*") + header('HTTP_AUTHORIZATION', "Basic #{encoded_login}") + end + + alias_method :authorize, :basic_authorize + + def digest_authorize(username, password) + @digest_username = username + @digest_password = password + end + + # Rack::Test will not follow any redirects automatically. This method + # will follow the redirect returned in the last response. If the last + # response was not a redirect, an error will be raised. + def follow_redirect! + unless last_response.redirect? + raise Error.new("Last response was not a redirect. Cannot follow_redirect!") + end + + get(last_response["Location"]) + end + + private + + def env_for(path, env) + uri = URI.parse(path) + uri.host ||= @default_host + + env = default_env.merge(env) + + env.update("HTTPS" => "on") if URI::HTTPS === uri + env["X-Requested-With"] = "XMLHttpRequest" if env[:xhr] + + if (env[:method] == "POST" || env["REQUEST_METHOD"] == "POST") && !env.has_key?(:input) + env["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + + multipart = (Hash === env[:params]) && + env[:params].any? { |_, v| UploadedFile === v } + + if multipart + env[:input] = multipart_body(env.delete(:params)) + env["CONTENT_LENGTH"] ||= env[:input].length.to_s + env["CONTENT_TYPE"] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}" + else + env[:input] = params_to_string(env.delete(:params)) + end + end + + params = env[:params] || {} + params.update(parse_query(uri.query)) + + uri.query = requestify(params) + + if env.has_key?(:cookie) + set_cookie(env.delete(:cookie), uri) + end + + Rack::MockRequest.env_for(uri.to_s, env) + end + + def process_request(uri, env) + uri = URI.parse(uri) + uri.host ||= @default_host + + @rack_mock_session.request(uri, env) + + if retry_with_digest_auth?(env) + auth_env = env.merge({ + "HTTP_AUTHORIZATION" => digest_auth_header, + "rack-test.digest_auth_retry" => true + }) + auth_env.delete('rack.request') + process_request(uri.path, auth_env) + else + yield last_response if block_given? + + last_response + end + end + + def digest_auth_header + challenge = last_response["WWW-Authenticate"].split(" ", 2).last + params = Rack::Auth::Digest::Params.parse(challenge) + + params.merge!({ + "username" => @digest_username, + "nc" => "00000001", + "cnonce" => "nonsensenonce", + "uri" => last_request.path_info, + "method" => last_request.env["REQUEST_METHOD"], + }) + + params["response"] = MockDigestRequest.new(params).response(@digest_password) + + "Digest #{params}" + end + + def retry_with_digest_auth?(env) + last_response.status == 401 && + digest_auth_configured? && + !env["rack-test.digest_auth_retry"] + end + + def digest_auth_configured? + @digest_username + end + + def default_env + { "rack.test" => true, "REMOTE_ADDR" => "127.0.0.1" }.merge(@headers) + end + + def params_to_string(params) + case params + when Hash then requestify(params) + when nil then "" + else params + end + end + + end + + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/cookie_jar.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/cookie_jar.rb new file mode 100644 index 0000000000..d58c914c9b --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/cookie_jar.rb @@ -0,0 +1,169 @@ +require "uri" +module Rack + module Test + + class Cookie + include Rack::Utils + + # :api: private + attr_reader :name, :value + + # :api: private + def initialize(raw, uri = nil, default_host = DEFAULT_HOST) + @default_host = default_host + uri ||= default_uri + + # separate the name / value pair from the cookie options + @name_value_raw, options = raw.split(/[;,] */n, 2) + + @name, @value = parse_query(@name_value_raw, ';').to_a.first + @options = parse_query(options, ';') + + @options["domain"] ||= (uri.host || default_host) + @options["path"] ||= uri.path.sub(/\/[^\/]*\Z/, "") + end + + def replaces?(other) + [name.downcase, domain, path] == [other.name.downcase, other.domain, other.path] + end + + # :api: private + def raw + @name_value_raw + end + + # :api: private + def empty? + @value.nil? || @value.empty? + end + + # :api: private + def domain + @options["domain"] + end + + def secure? + @options.has_key?("secure") + end + + # :api: private + def path + @options["path"].strip || "/" + end + + # :api: private + def expires + Time.parse(@options["expires"]) if @options["expires"] + end + + # :api: private + def expired? + expires && expires < Time.now + end + + # :api: private + def valid?(uri) + uri ||= default_uri + + if uri.host.nil? + uri.host = @default_host + end + + (!secure? || (secure? && uri.scheme == "https")) && + uri.host =~ Regexp.new("#{Regexp.escape(domain)}$", Regexp::IGNORECASE) && + uri.path =~ Regexp.new("^#{Regexp.escape(path)}") + end + + # :api: private + def matches?(uri) + ! expired? && valid?(uri) + end + + # :api: private + def <=>(other) + # Orders the cookies from least specific to most + [name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse] + end + + protected + + def default_uri + URI.parse("//" + @default_host + "/") + end + + end + + class CookieJar + + # :api: private + def initialize(cookies = [], default_host = DEFAULT_HOST) + @default_host = default_host + @cookies = cookies + @cookies.sort! + end + + def [](name) + cookies = hash_for(nil) + # TODO: Should be case insensitive + cookies[name] && cookies[name].value + end + + def []=(name, value) + # TODO: needs proper escaping + merge("#{name}=#{value}") + end + + def merge(raw_cookies, uri = nil) + return unless raw_cookies + + raw_cookies.each_line do |raw_cookie| + cookie = Cookie.new(raw_cookie, uri, @default_host) + self << cookie if cookie.valid?(uri) + end + end + + def <<(new_cookie) + @cookies.reject! do |existing_cookie| + new_cookie.replaces?(existing_cookie) + end + + @cookies << new_cookie + @cookies.sort! + end + + # :api: private + def for(uri) + hash_for(uri).values.map { |c| c.raw }.join(';') + end + + def to_hash + cookies = {} + + hash_for(nil).each do |name, cookie| + cookies[name] = cookie.value + end + + return cookies + end + + protected + + def hash_for(uri = nil) + cookies = {} + + # The cookies are sorted by most specific first. So, we loop through + # all the cookies in order and add it to a hash by cookie name if + # the cookie can be sent to the current URI. It's added to the hash + # so that when we are done, the cookies will be unique by name and + # we'll have grabbed the most specific to the URI. + @cookies.each do |cookie| + cookies[cookie.name] = cookie if cookie.matches?(uri) + end + + return cookies + end + + end + + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/methods.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/methods.rb new file mode 100644 index 0000000000..a191fa23d8 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/methods.rb @@ -0,0 +1,45 @@ +require "forwardable" + +module Rack + module Test + module Methods + extend Forwardable + + def rack_test_session + @_rack_test_session ||= Rack::Test::Session.new(app) + end + + def rack_mock_session + @_rack_mock_session ||= Rack::MockSession.new(app) + end + + METHODS = [ + :request, + + # HTTP verbs + :get, + :post, + :put, + :delete, + :head, + + # Redirects + :follow_redirect!, + + # Header-related features + :header, + :set_cookie, + :clear_cookies, + :authorize, + :basic_authorize, + :digest_authorize, + + # Expose the last request and response + :last_response, + :last_request + ] + + def_delegators :rack_test_session, *METHODS + end + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/mock_digest_request.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/mock_digest_request.rb new file mode 100644 index 0000000000..81c398ba51 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/mock_digest_request.rb @@ -0,0 +1,27 @@ +module Rack + module Test + + class MockDigestRequest + def initialize(params) + @params = params + end + + def method_missing(sym) + if @params.has_key? k = sym.to_s + return @params[k] + end + + super + end + + def method + @params['method'] + end + + def response(password) + Rack::Auth::Digest::MD5.new(nil).send :digest, self, password + end + end + + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/uploaded_file.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/uploaded_file.rb new file mode 100644 index 0000000000..239302fbe4 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/uploaded_file.rb @@ -0,0 +1,36 @@ +require "tempfile" + +module Rack + module Test + + class UploadedFile + # The filename, *not* including the path, of the "uploaded" file + attr_reader :original_filename + + # The content type of the "uploaded" file + attr_accessor :content_type + + def initialize(path, content_type = "text/plain", binary = false) + raise "#{path} file does not exist" unless ::File.exist?(path) + @content_type = content_type + @original_filename = ::File.basename(path) + @tempfile = Tempfile.new(@original_filename) + @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding) + @tempfile.binmode if binary + FileUtils.copy_file(path, @tempfile.path) + end + + def path + @tempfile.path + end + + alias_method :local_path, :path + + def method_missing(method_name, *args, &block) #:nodoc: + @tempfile.__send__(method_name, *args, &block) + end + + end + + end +end diff --git a/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/utils.rb b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/utils.rb new file mode 100644 index 0000000000..d25b849709 --- /dev/null +++ b/actionpack/lib/action_dispatch/vendor/rack-test/rack/test/utils.rb @@ -0,0 +1,75 @@ +module Rack + module Test + + module Utils + include Rack::Utils + + def requestify(value, prefix = nil) + case value + when Array + value.map do |v| + requestify(v, "#{prefix}[]") + end.join("&") + when Hash + value.map do |k, v| + requestify(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) + end.join("&") + else + "#{prefix}=#{escape(value)}" + end + end + + module_function :requestify + + def multipart_requestify(params, first=true) + p = Hash.new + + params.each do |key, value| + k = first ? key.to_s : "[#{key}]" + + if Hash === value + multipart_requestify(value, false).each do |subkey, subvalue| + p[k + subkey] = subvalue + end + else + p[k] = value + end + end + + return p + end + + module_function :multipart_requestify + + def multipart_body(params) + multipart_requestify(params).map do |key, value| + if value.respond_to?(:original_filename) + ::File.open(value.path, "rb") do |f| + f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) + + <<-EOF +--#{MULTIPART_BOUNDARY}\r +Content-Disposition: form-data; name="#{key}"; filename="#{escape(value.original_filename)}"\r +Content-Type: #{value.content_type}\r +Content-Length: #{::File.stat(value.path).size}\r +\r +#{f.read}\r +EOF + end + else +<<-EOF +--#{MULTIPART_BOUNDARY}\r +Content-Disposition: form-data; name="#{key}"\r +\r +#{value}\r +EOF + end + end.join("")+"--#{MULTIPART_BOUNDARY}--\r" + end + + module_function :multipart_body + + end + + end +end |