Skip to content
Browse files

Merge branch 'master' of git@github.com:rails/rails

  • Loading branch information...
2 parents 2e053ae + 490c26c commit 276ec16007b03d0a527fb0b83a7ee0b81e460fa1 @jeremy jeremy committed Dec 29, 2008
Showing with 4,177 additions and 1,651 deletions.
  1. +3 −1 actionmailer/lib/action_mailer/base.rb
  2. +6 −3 actionmailer/test/abstract_unit.rb
  3. +14 −0 actionpack/CHANGELOG
  4. +7 −2 actionpack/lib/action_controller.rb
  5. +1 −1 actionpack/lib/action_controller/assertions/routing_assertions.rb
  6. +1 −0 actionpack/lib/action_controller/assertions/selector_assertions.rb
  7. +51 −21 actionpack/lib/action_controller/base.rb
  8. +1 −2 actionpack/lib/action_controller/caching.rb
  9. +0 −18 actionpack/lib/action_controller/caching/sql_cache.rb
  10. +12 −26 actionpack/lib/action_controller/cookies.rb
  11. +6 −23 actionpack/lib/action_controller/dispatcher.rb
  12. +3 −3 actionpack/lib/action_controller/helpers.rb
  13. +189 −2 actionpack/lib/action_controller/http_authentication.rb
  14. +94 −1 actionpack/lib/action_controller/integration.rb
  15. +18 −4 actionpack/lib/action_controller/layout.rb
  16. +21 −0 actionpack/lib/action_controller/middlewares.rb
  17. +19 −4 actionpack/lib/action_controller/mime_responds.rb
  18. +11 −7 actionpack/lib/action_controller/polymorphic_routes.rb
  19. +0 −73 actionpack/lib/action_controller/rack_process.rb
  20. +62 −450 actionpack/lib/action_controller/request.rb
  21. +314 −0 actionpack/lib/action_controller/request_parser.rb
  22. +3 −1 actionpack/lib/action_controller/rescue.rb
  23. +34 −20 actionpack/lib/action_controller/response.rb
  24. +37 −30 actionpack/lib/action_controller/routing/route_set.rb
  25. +12 −3 actionpack/lib/action_controller/streaming.rb
  26. +1 −4 actionpack/lib/action_controller/test_case.rb
  27. +50 −30 actionpack/lib/action_controller/test_process.rb
  28. +37 −0 actionpack/lib/action_controller/uploaded_file.rb
  29. +95 −0 actionpack/lib/action_controller/url_encoded_pair_parser.rb
  30. +24 −0 actionpack/lib/action_controller/verb_piggybacking.rb
  31. +5 −57 actionpack/lib/action_view/base.rb
  32. +75 −0 actionpack/lib/action_view/helpers/date_helper.rb
  33. +7 −3 actionpack/lib/action_view/helpers/form_helper.rb
  34. +0 −11 actionpack/lib/action_view/helpers/prototype_helper.rb
  35. +7 −0 actionpack/lib/action_view/locale/en.yml
  36. +1 −1 actionpack/lib/action_view/partials.rb
  37. +13 −28 actionpack/lib/action_view/paths.rb
  38. +12 −10 actionpack/lib/action_view/renderable.rb
  39. +28 −0 actionpack/lib/action_view/template.rb
  40. +8 −0 actionpack/test/controller/assert_select_test.rb
  41. +1 −2 actionpack/test/controller/caching_test.rb
  42. +17 −55 actionpack/test/controller/cookie_test.rb
  43. +1 −3 actionpack/test/controller/dispatcher_test.rb
  44. +73 −0 actionpack/test/controller/http_digest_authentication_test.rb
  45. +88 −0 actionpack/test/controller/integration_test.rb
  46. +12 −10 actionpack/test/controller/layout_test.rb
  47. +5 −28 actionpack/test/controller/rack_test.rb
  48. +72 −8 actionpack/test/controller/render_test.rb
  49. +39 −41 actionpack/test/controller/request_test.rb
  50. +5 −1 actionpack/test/controller/rescue_test.rb
  51. +60 −73 actionpack/test/controller/routing_test.rb
  52. +25 −0 actionpack/test/controller/send_file_test.rb
  53. +4 −5 actionpack/test/controller/session/cookie_store_test.rb
  54. +11 −0 actionpack/test/template/date_helper_i18n_test.rb
  55. +321 −0 actionpack/test/template/date_helper_test.rb
  56. +4 −0 activerecord/CHANGELOG
  57. +1 −0 activerecord/lib/active_record.rb
  58. +20 −15 activerecord/lib/active_record/association_preload.rb
  59. +19 −19 activerecord/lib/active_record/associations.rb
  60. +82 −8 activerecord/lib/active_record/base.rb
  61. +6 −6 activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
  62. +24 −24 activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
  63. +13 −11 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
  64. +7 −7 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
  65. +1 −1 activerecord/lib/active_record/dirty.rb
  66. +25 −0 activerecord/lib/active_record/dynamic_scope_match.rb
  67. +25 −13 activerecord/lib/active_record/query_cache.rb
  68. +2 −2 activerecord/lib/active_record/timestamp.rb
  69. +0 −1 activerecord/lib/active_record/validations.rb
  70. +8 −0 activerecord/test/cases/associations/cascaded_eager_loading_test.rb
  71. +17 −2 activerecord/test/cases/associations/eager_test.rb
  72. +13 −0 activerecord/test/cases/associations/has_many_associations_test.rb
  73. +15 −0 activerecord/test/cases/base_test.rb
  74. +18 −0 activerecord/test/cases/method_scoping_test.rb
  75. +20 −0 activerecord/test/cases/named_scope_test.rb
  76. +10 −1 activerecord/test/fixtures/people.yml
  77. +1 −0 activerecord/test/models/company.rb
  78. +6 −0 activerecord/test/models/person.rb
  79. +4 −2 activerecord/test/schema/schema.rb
  80. +8 −0 activeresource/lib/active_resource/http_mock.rb
  81. +4 −0 activesupport/CHANGELOG
  82. +7 −7 activesupport/lib/active_support/buffered_logger.rb
  83. +20 −21 activesupport/lib/active_support/callbacks.rb
  84. +24 −24 activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
  85. +20 −19 activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
  86. +34 −34 activesupport/lib/active_support/core_ext/class/inheritable_attributes.rb
  87. +8 −1 activesupport/lib/active_support/core_ext/hash/slice.rb
  88. +6 −6 activesupport/lib/active_support/core_ext/logger.rb
  89. +3 −3 activesupport/lib/active_support/core_ext/module/aliasing.rb
  90. +4 −4 activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb
  91. +24 −24 activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
  92. +29 −3 activesupport/lib/active_support/core_ext/module/delegation.rb
  93. +5 −5 activesupport/lib/active_support/core_ext/module/synchronization.rb
  94. +9 −4 activesupport/lib/active_support/deprecation.rb
  95. +30 −28 activesupport/lib/active_support/memoizable.rb
  96. +4 −4 activesupport/lib/active_support/multibyte/unicode_database.rb
  97. +3 −3 activesupport/lib/active_support/time_with_zone.rb
  98. +2 −2 activesupport/lib/active_support/vendor.rb
  99. +3 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore
  100. +20 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE
  101. +20 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile
  102. +5 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile
  103. +27 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec
  104. +37 −37 activesupport/lib/active_support/vendor/{i18n-0.0.1 → i18n-0.1.1/lib}/i18n.rb
  105. +29 −29 activesupport/lib/active_support/vendor/{i18n-0.0.1 → i18n-0.1.1/lib}/i18n/backend/simple.rb
  106. +3 −3 activesupport/lib/active_support/vendor/{i18n-0.0.1 → i18n-0.1.1/lib}/i18n/exceptions.rb
  107. +5 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb
  108. +100 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb
  109. +125 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb
  110. +1 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb
  111. +3 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml
  112. +502 −0 activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb
  113. +41 −1 activesupport/test/callbacks_test.rb
  114. +17 −3 activesupport/test/core_ext/hash_ext_test.rb
  115. +27 −0 activesupport/test/core_ext/module_test.rb
  116. +116 −29 railties/doc/guides/source/command_line.txt
  117. +108 −44 railties/doc/guides/source/finders.txt
  118. +110 −11 railties/doc/guides/source/form_helpers.txt
  119. +1 −1 railties/lib/commands/dbconsole.rb
  120. +1 −1 railties/lib/rails_generator/commands.rb
  121. +64 −67 railties/lib/rails_generator/generators/applications/app/template_runner.rb
  122. +2 −2 railties/lib/tasks/tmp.rake
  123. +7 −42 railties/test/fcgi_dispatcher_test.rb
  124. +12 −12 railties/test/gem_dependency_test.rb
  125. +190 −0 railties/test/generators/rails_template_runner_test.rb
View
4 actionmailer/lib/action_mailer/base.rb
@@ -570,7 +570,9 @@ def default_template_format
end
def candidate_for_layout?(options)
- !@template.send(:_exempt_from_layout?, default_template_name)
+ !self.view_paths.find_template(default_template_name, default_template_format).exempt_from_layout?
+ rescue ActionView::MissingTemplate
+ return true
end
def template_root
View
9 actionmailer/test/abstract_unit.rb
@@ -10,11 +10,14 @@
ActiveSupport::Deprecation.debug = true
# Bogus template processors
-ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!" }
-ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup" }
+ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect }
+ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect }
$:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers"
-ActionMailer::Base.template_root = "#{File.dirname(__FILE__)}/fixtures"
+
+FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
+ActionMailer::Base.template_root = FIXTURE_LOAD_PATH
+ActionMailer::Base.template_root.load
class MockSMTP
def self.deliveries
View
14 actionpack/CHANGELOG
@@ -1,5 +1,19 @@
*2.3.0 [Edge]*
+* Make ActionController#render(string) work as a shortcut for render :file/:template/:action => string. [#1435] [Pratik Naik] Examples:
+
+ # Instead of render(:action => 'other_action')
+ render('other_action') # argument has no '/'
+ render(:other_action)
+
+ # Instead of render(:template => 'controller/action')
+ render('controller/action') # argument must not begin with a '/', but contain a '/'
+
+ # Instead of render(:file => '/Users/lifo/home.html.erb')
+ render('/Users/lifo/home.html.erb') # argument must begin with a '/'
+
+* Add :prompt option to date/time select helpers. #561 [Sam Oliver]
+
* Fixed that send_file shouldn't set an etag #1578 [Hongli Lai]
* Allow users to opt out of the spoofing checks in Request#remote_ip. Useful for sites whose traffic regularly triggers false positives. [Darren Boyd]
View
9 actionpack/lib/action_controller.rb
@@ -38,7 +38,7 @@ module ActionController
# TODO: Review explicit to see if they will automatically be handled by
# the initilizer if they are really needed.
def self.load_all!
- [Base, CGIHandler, CgiRequest, RackRequest, RackRequest, Http::Headers, UrlRewriter, UrlWriter]
+ [Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
end
autoload :AbstractRequest, 'action_controller/request'
@@ -59,7 +59,11 @@ def self.load_all!
autoload :MiddlewareStack, 'action_controller/middleware_stack'
autoload :MimeResponds, 'action_controller/mime_responds'
autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes'
- autoload :RackRequest, 'action_controller/rack_process'
+ autoload :Request, 'action_controller/request'
+ autoload :RequestParser, 'action_controller/request_parser'
+ autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser'
+ autoload :UploadedStringIO, 'action_controller/uploaded_file'
+ autoload :UploadedTempfile, 'action_controller/uploaded_file'
autoload :RecordIdentifier, 'action_controller/record_identifier'
autoload :Response, 'action_controller/response'
autoload :RequestForgeryProtection, 'action_controller/request_forgery_protection'
@@ -74,6 +78,7 @@ def self.load_all!
autoload :Translation, 'action_controller/translation'
autoload :UrlRewriter, 'action_controller/url_rewriter'
autoload :UrlWriter, 'action_controller/url_rewriter'
+ autoload :VerbPiggybacking, 'action_controller/verb_piggybacking'
autoload :Verification, 'action_controller/verification'
module Assertions
View
2 actionpack/lib/action_controller/assertions/routing_assertions.rb
@@ -134,7 +134,7 @@ def recognized_request_for(path, request_method = nil)
path = "/#{path}" unless path.first == '/'
# Assume given controller
- request = ActionController::TestRequest.new({}, {}, nil)
+ request = ActionController::TestRequest.new
request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path
View
1 actionpack/lib/action_controller/assertions/selector_assertions.rb
@@ -402,6 +402,7 @@ def assert_select_rjs(*args, &block)
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]})"
View
72 actionpack/lib/action_controller/base.rb
@@ -254,7 +254,7 @@ class Base
cattr_reader :protected_instance_variables
# Controller specific instance variables which will not be accessible inside views.
@@protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller
- @action_name @before_filter_chain_aborted @action_cache_path @_session @_cookies @_headers @_params
+ @action_name @before_filter_chain_aborted @action_cache_path @_session @_headers @_params
@_flash @_response)
# Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets,
@@ -382,6 +382,13 @@ class Base
attr_accessor :action_name
class << self
+ def call(env)
+ # HACK: For global rescue to have access to the original request and response
+ request = env["actioncontroller.rescue.request"] ||= Request.new(env)
+ response = env["actioncontroller.rescue.response"] ||= Response.new
+ process(request, response)
+ end
+
# Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc:
new.process(request, response)
@@ -502,7 +509,7 @@ def filter_parameter_logging(*filter_words, &block)
protected :filter_parameters
end
- delegate :exempt_from_layout, :to => 'ActionView::Base'
+ delegate :exempt_from_layout, :to => 'ActionView::Template'
end
public
@@ -859,16 +866,23 @@ def append_view_path(path)
def render(options = nil, extra_options = {}, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed?
+ validate_render_arguments(options, extra_options, block_given?)
+
if options.nil?
- return render(:file => default_template_name, :layout => true)
- elsif !extra_options.is_a?(Hash)
- raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
- else
- if options == :update
- options = extra_options.merge({ :update => true })
- elsif !options.is_a?(Hash)
- raise RenderError, "You called render with invalid options : #{options.inspect}"
+ options = { :template => default_template.filename, :layout => true }
+ elsif options == :update
+ options = extra_options.merge({ :update => true })
+ elsif options.is_a?(String) || options.is_a?(Symbol)
+ case options.to_s.index('/')
+ when 0
+ extra_options[:file] = options
+ when nil
+ extra_options[:action] = options
+ else
+ extra_options[:template] = options
end
+
+ options = extra_options
end
layout = pick_layout(options)
@@ -898,7 +912,7 @@ def render(options = nil, extra_options = {}, &block) #:doc:
render_for_text(@template.render(options.merge(:layout => layout)), options[:status])
elsif action_name = options[:action]
- render_for_file(default_template_name(action_name.to_s), options[:status], layout)
+ render_for_file(default_template(action_name.to_s), options[:status], layout)
elsif xml = options[:xml]
response.content_type ||= Mime::XML
@@ -933,7 +947,7 @@ def render(options = nil, extra_options = {}, &block) #:doc:
render_for_text(nil, options[:status])
else
- render_for_file(default_template_name, options[:status], layout)
+ render_for_file(default_template, options[:status], layout)
end
end
end
@@ -1164,7 +1178,8 @@ def reset_session #:doc:
private
def render_for_file(template_path, status = nil, layout = nil, locals = {}) #:nodoc:
- logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
+ path = template_path.respond_to?(:path_without_format_and_extension) ? template_path.path_without_format_and_extension : template_path
+ logger.info("Rendering #{path}" + (status ? " (#{status})" : '')) if logger
render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status
end
@@ -1185,6 +1200,16 @@ def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
end
end
+ def validate_render_arguments(options, extra_options, has_block)
+ if options && (has_block && options != :update) && !options.is_a?(String) && !options.is_a?(Hash) && !options.is_a?(Symbol)
+ raise RenderError, "You called render with invalid options : #{options.inspect}"
+ end
+
+ if !extra_options.is_a?(Hash)
+ raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
+ end
+ end
+
def initialize_template_class(response)
response.template = ActionView::Base.new(self.class.view_paths, {}, self)
response.template.helpers.send :include, self.class.master_helper_module
@@ -1193,7 +1218,7 @@ def initialize_template_class(response)
end
def assign_shortcuts(request, response)
- @_request, @_params, @_cookies = request, request.parameters, request.cookies
+ @_request, @_params = request, request.parameters
@_response = response
@_response.session = request.session
@@ -1241,10 +1266,17 @@ def perform_action
elsif respond_to? :method_missing
method_missing action_name
default_render unless performed?
- elsif template_exists?
- default_render
else
- raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
+ begin
+ default_render
+ rescue ActionView::MissingTemplate => e
+ # Was the implicit template missing, or was it another template?
+ if e.path == default_template_name
+ raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
+ else
+ raise e
+ end
+ end
end
end
@@ -1290,10 +1322,8 @@ def close_session
@_session.close if @_session && @_session.respond_to?(:close)
end
- def template_exists?(template_name = default_template_name)
- @template.send(:_pick_template, template_name) ? true : false
- rescue ActionView::MissingTemplate
- false
+ def default_template(action_name = self.action_name)
+ self.view_paths.find_template(default_template_name(action_name), default_template_format)
end
def default_template_name(action_name = self.action_name)
View
3 actionpack/lib/action_controller/caching.rb
@@ -27,7 +27,6 @@ module Caching
autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages'
- autoload :SqlCache, 'action_controller/caching/sql_cache'
autoload :Sweeping, 'action_controller/caching/sweeping'
def self.included(base) #:nodoc:
@@ -41,7 +40,7 @@ def self.cache_store=(store_option)
end
include Pages, Actions, Fragments
- include Sweeping, SqlCache if defined?(ActiveRecord)
+ include Sweeping if defined?(ActiveRecord)
@@perform_caching = true
cattr_accessor :perform_caching
View
18 actionpack/lib/action_controller/caching/sql_cache.rb
@@ -1,18 +0,0 @@
-module ActionController #:nodoc:
- module Caching
- module SqlCache
- def self.included(base) #:nodoc:
- if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache)
- base.alias_method_chain :perform_action, :caching
- end
- end
-
- protected
- def perform_action_with_caching
- ActiveRecord::Base.cache do
- perform_action_without_caching
- end
- end
- end
- end
-end
View
38 actionpack/lib/action_controller/cookies.rb
@@ -64,45 +64,31 @@ def initialize(controller)
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
def [](name)
- cookie = @cookies[name.to_s]
- if cookie && cookie.respond_to?(:value)
- cookie.size > 1 ? cookie.value : cookie.value[0]
- else
- cookie
- end
+ super(name.to_s)
end
# Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above.
- def []=(name, options)
+ def []=(key, options)
if options.is_a?(Hash)
- options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
- options["name"] = name.to_s
+ options.symbolize_keys!
else
- options = { "name" => name.to_s, "value" => options }
+ options = { :value => options }
end
- set_cookie(options)
+ options[:path] = "/" unless options.has_key?(:path)
+ super(key.to_s, options[:value])
+ @controller.response.set_cookie(key, options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
- def delete(name, options = {})
- options.stringify_keys!
- set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0)))
+ def delete(key, options = {})
+ options.symbolize_keys!
+ options[:path] = "/" unless options.has_key?(:path)
+ super(key.to_s)
+ @controller.response.delete_cookie(key, options)
end
-
- private
- # Builds a CGI::Cookie object and adds the cookie to the response headers.
- #
- # The path of the cookie defaults to "/" if there's none in +options+, and
- # everything is passed to the CGI::Cookie constructor.
- def set_cookie(options) #:doc:
- options["path"] = "/" unless options["path"]
- cookie = CGI::Cookie.new(options)
- @controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil?
- @controller.response.headers["cookie"] << cookie
- end
end
end
View
29 actionpack/lib/action_controller/dispatcher.rb
@@ -44,22 +44,8 @@ def to_prepare(identifier = nil, &block)
cattr_accessor :middleware
self.middleware = MiddlewareStack.new do |middleware|
- middleware.use "ActionController::Lock", :if => lambda {
- !ActionController::Base.allow_concurrency
- }
- middleware.use "ActionController::Failsafe"
-
- ["ActionController::Session::CookieStore",
- "ActionController::Session::MemCacheStore",
- "ActiveRecord::SessionStore"].each do |store|
- middleware.use(store, ActionController::Base.session_options,
- :if => lambda {
- if session_store = ActionController::Base.session_store
- session_store.name == store
- end
- }
- )
- end
+ middlewares = File.join(File.dirname(__FILE__), "middlewares.rb")
+ middleware.instance_eval(File.read(middlewares))
end
include ActiveSupport::Callbacks
@@ -74,11 +60,10 @@ def initialize(output = $stdout, request = nil, response = nil)
def dispatch
begin
run_callbacks :before_dispatch
- controller = Routing::Routes.recognize(@request)
- controller.process(@request, @response).to_a
+ Routing::Routes.call(@env)
rescue Exception => exception
if controller ||= (::ApplicationController rescue Base)
- controller.process_with_exception(@request, @response, exception).to_a
+ controller.call_with_exception(@env, exception).to_a
else
raise exception
end
@@ -97,8 +82,7 @@ def call(env)
end
def _call(env)
- @request = RackRequest.new(env)
- @response = Response.new
+ @env = env
dispatch
end
@@ -124,8 +108,7 @@ def flush_logger
def checkin_connections
# Don't return connection (and peform implicit rollback) if this request is a part of integration test
- # TODO: This callback should have direct access to env
- return if @request.key?("rack.test")
+ return if @env.key?("rack.test")
ActiveRecord::Base.clear_active_connections!
end
end
View
6 actionpack/lib/action_controller/helpers.rb
@@ -163,9 +163,9 @@ def helper(*args, &block)
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
- def #{method}(*args, &block)
- controller.send(%(#{method}), *args, &block)
- end
+ def #{method}(*args, &block) # def current_user(*args, &block)
+ controller.send(%(#{method}), *args, &block) # controller.send(%(current_user), *args, &block)
+ end # end
end_eval
end
end
View
191 actionpack/lib/action_controller/http_authentication.rb
@@ -55,7 +55,31 @@ module HttpAuthentication
# end
# end
#
- #
+ # Simple Digest example. Note the block must return the user's password so the framework
+ # can appropriately hash it to check the user's credentials. Returning nil will cause authentication to fail.
+ #
+ # class PostsController < ApplicationController
+ # Users = {"dhh" => "secret"}
+ #
+ # before_filter :authenticate, :except => [ :index ]
+ #
+ # def index
+ # render :text => "Everyone can see me!"
+ # end
+ #
+ # def edit
+ # render :text => "I'm only accessible if you know the password"
+ # end
+ #
+ # private
+ # def authenticate
+ # authenticate_or_request_with_http_digest(realm) do |user_name|
+ # Users[user_name]
+ # end
+ # end
+ # end
+ #
+ #
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
@@ -108,7 +132,10 @@ def authorization(request)
end
def decode_credentials(request)
- ActiveSupport::Base64.decode64(authorization(request).split.last || '')
+ # Properly decode credentials spanning a new-line
+ auth = authorization(request)
+ auth.slice!('Basic ')
+ ActiveSupport::Base64.decode64(auth || '')
end
def encode_credentials(user_name, password)
@@ -120,5 +147,165 @@ def authentication_request(controller, realm)
controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end
end
+
+ module Digest
+ extend self
+
+ module ControllerMethods
+ def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
+ begin
+ authenticate_with_http_digest!(realm, &password_procedure)
+ rescue ActionController::HttpAuthentication::Error => e
+ msg = e.message
+ msg = "#{msg} expected '#{e.expected}' was '#{e.was}'" unless e.expected.nil?
+ raise msg if e.fatal?
+ request_http_digest_authentication(realm, msg)
+ end
+ end
+
+ # Authenticate using HTTP Digest, throwing ActionController::HttpAuthentication::Error on failure.
+ # This allows more detailed analysis of authentication failures
+ # to be relayed to the client.
+ def authenticate_with_http_digest!(realm = "Application", &login_procedure)
+ HttpAuthentication::Digest.authenticate(self, realm, &login_procedure)
+ end
+
+ # Authenticate with HTTP Digest, returns true or false
+ def authenticate_with_http_digest(realm = "Application", &login_procedure)
+ HttpAuthentication::Digest.authenticate(self, realm, &login_procedure) rescue false
+ end
+
+ # Render output including the HTTP Digest authentication header
+ def request_http_digest_authentication(realm = "Application", message = nil)
+ HttpAuthentication::Digest.authentication_request(self, realm, message)
+ end
+
+ # Add HTTP Digest authentication header to result headers
+ def http_digest_authentication_header(realm = "Application")
+ HttpAuthentication::Digest.authentication_header(self, realm)
+ end
+ end
+
+ # Raises error unless authentictaion succeeds, returns true otherwise
+ def authenticate(controller, realm, &password_procedure)
+ raise Error.new(false), "No authorization header found" unless authorization(controller.request)
+ validate_digest_response(controller, realm, &password_procedure)
+ true
+ end
+
+ def authorization(request)
+ request.env['HTTP_AUTHORIZATION'] ||
+ request.env['X-HTTP_AUTHORIZATION'] ||
+ request.env['X_HTTP_AUTHORIZATION'] ||
+ request.env['REDIRECT_X_HTTP_AUTHORIZATION']
+ end
+
+ # Raises error unless the request credentials response value matches the expected value.
+ def validate_digest_response(controller, realm, &password_procedure)
+ credentials = decode_credentials(controller.request)
+
+ # Check the nonce, opaque and realm.
+ # Ignore nc, as we have no way to validate the number of times this nonce has been used
+ validate_nonce(controller.request, credentials[:nonce])
+ raise Error.new(false, realm, credentials[:realm]), "Realm doesn't match" unless realm == credentials[:realm]
+ raise Error.new(true, opaque(controller.request), credentials[:opaque]),"Opaque doesn't match" unless opaque(controller.request) == credentials[:opaque]
+
+ password = password_procedure.call(credentials[:username])
+ raise Error.new(false), "No password" if password.nil?
+ expected = expected_response(controller.request.env['REQUEST_METHOD'], controller.request.url, credentials, password)
+ raise Error.new(false, expected, credentials[:response]), "Invalid response" unless expected == credentials[:response]
+ end
+
+ # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
+ def expected_response(http_method, uri, credentials, password)
+ ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
+ ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase,uri].join(':'))
+ ::Digest::MD5.hexdigest([ha1,credentials[:nonce], credentials[:nc], credentials[:cnonce],credentials[:qop],ha2].join(':'))
+ end
+
+ def encode_credentials(http_method, credentials, password)
+ credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password)
+ "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
+ end
+
+ def decode_credentials(request)
+ authorization(request).to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
+ key, value = pair.split('=', 2)
+ hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
+ hash
+ end
+ end
+
+ def authentication_header(controller, realm)
+ controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(controller.request)}", opaque="#{opaque(controller.request)}")
+ end
+
+ def authentication_request(controller, realm, message = "HTTP Digest: Access denied")
+ authentication_header(controller, realm)
+ controller.send! :render, :text => message, :status => :unauthorized
+ end
+
+ # Uses an MD5 digest based on time to generate a value to be used only once.
+ #
+ # A server-specified data string which should be uniquely generated each time a 401 response is made.
+ # It is recommended that this string be base64 or hexadecimal data.
+ # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
+ #
+ # The contents of the nonce are implementation dependent.
+ # The quality of the implementation depends on a good choice.
+ # A nonce might, for example, be constructed as the base 64 encoding of
+ #
+ # => time-stamp H(time-stamp ":" ETag ":" private-key)
+ #
+ # where time-stamp is a server-generated time or other non-repeating value,
+ # ETag is the value of the HTTP ETag header associated with the requested entity,
+ # and private-key is data known only to the server.
+ # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
+ # reject the request if it did not match the nonce from that header or
+ # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
+ # The inclusion of the ETag prevents a replay request for an updated version of the resource.
+ # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
+ # to limit the reuse of the nonce to the same client that originally got it.
+ # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
+ # Also, IP address spoofing is not that hard.)
+ #
+ # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
+ # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
+ # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
+ # of this document.
+ #
+ # The nonce is opaque to the client.
+ def nonce(request, time = Time.now)
+ session_id = request.is_a?(String) ? request : request.session.session_id
+ t = time.to_i
+ hashed = [t, session_id]
+ digest = ::Digest::MD5.hexdigest(hashed.join(":"))
+ Base64.encode64("#{t}:#{digest}").gsub("\n", '')
+ end
+
+ def validate_nonce(request, value)
+ t = Base64.decode64(value).split(":").first.to_i
+ raise Error.new(true), "Stale Nonce" if (t - Time.now.to_i).abs > 10 * 60
+ n = nonce(request, t)
+ raise Error.new(true, value, n), "Bad Nonce" unless n == value
+ end
+
+ # Opaque based on digest of session_id
+ def opaque(request)
+ session_id = request.is_a?(String) ? request : request.session.session_id
+ @opaque ||= Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '')
+ end
+ end
+
+ class Error < RuntimeError
+ attr_accessor :expected, :was
+ def initialize(fatal = false, expected = nil, was = nil)
+ @fatal = fatal
+ @expected = expected
+ @was = was
+ end
+
+ def fatal?; @fatal; end
+ end
end
end
View
95 actionpack/lib/action_controller/integration.rb
@@ -2,6 +2,17 @@
require 'uri'
require 'active_support/test_case'
+# Monkey patch Rack::Lint to support rewind
+module Rack
+ class Lint
+ class InputWrapper
+ def rewind
+ @input.rewind
+ end
+ end
+ end
+end
+
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
@@ -57,6 +68,15 @@ class Session
# A running counter of the number of requests processed.
attr_accessor :request_count
+ # Nonce value for Digest Authentication, implicitly set on response with WWW-Authentication
+ attr_accessor :nonce
+
+ # Opaque value for Digest Authentication, implicitly set on response with WWW-Authentication
+ attr_accessor :opaque
+
+ # Opaque value for Authentication, implicitly set on response with WWW-Authentication
+ attr_accessor :realm
+
class MultiPartNeededException < Exception
end
@@ -232,6 +252,53 @@ def xml_http_request(request_method, path, parameters = nil, headers = nil)
end
alias xhr :xml_http_request
+ def request_with_noauth(http_method, uri, parameters, headers)
+ process_with_auth http_method, uri, parameters, headers
+ end
+
+ # Performs a request with the given http_method and parameters, including HTTP Basic authorization headers.
+ # See get() for more details on paramters and headers.
+ #
+ # You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_basic, #post_with_basic,
+ # #put_with_basic, #delete_with_basic, and #head_with_basic.
+ def request_with_basic(http_method, uri, parameters, headers, user_name, password)
+ process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password))
+ end
+
+ # Performs a request with the given http_method and parameters, including HTTP Digest authorization headers.
+ # See get() for more details on paramters and headers.
+ #
+ # You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_digest, #post_with_digest,
+ # #put_with_digest, #delete_with_digest, and #head_with_digest.
+ def request_with_digest(http_method, uri, parameters, headers, user_name, password)
+ # Realm, Nonce, and Opaque taken from previoius 401 response
+
+ credentials = {
+ :username => user_name,
+ :realm => @realm,
+ :nonce => @nonce,
+ :qop => "auth",
+ :nc => "00000001",
+ :cnonce => "0a4f113b",
+ :opaque => @opaque,
+ :uri => uri
+ }
+
+ raise "Digest request without previous 401 response" if @opaque.nil?
+
+ process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Digest.encode_credentials(http_method, credentials, password))
+ end
+
+ # def get_with_basic, def post_with_basic, def put_with_basic, def delete_with_basic, def head_with_basic
+ # def get_with_digest, def post_with_digest, def put_with_digest, def delete_with_digest, def head_with_digest
+ [:get, :post, :put, :delete, :head].each do |method|
+ [:noauth, :basic, :digest].each do |auth_type|
+ define_method("#{method}_with_#{auth_type}") do |uri, parameters, headers, *auth|
+ send("request_with_#{auth_type}", method, uri, parameters, headers, *auth)
+ end
+ end
+ end
+
# Returns the URL for the given options, according to the rules specified
# in the application's routes.
def url_for(options)
@@ -353,6 +420,32 @@ def process(method, path, parameters = nil, headers = nil)
return status
end
+ # Same as process, but handles authentication returns to perform
+ # Basic or Digest authentication
+ def process_with_auth(method, path, parameters = nil, headers = nil)
+ status = process(method, path, parameters, headers)
+
+ if status == 401
+ # Extract authentication information from response
+ auth_data = @response.headers['WWW-Authenticate']
+ if /^Basic /.match(auth_data)
+ # extract realm, to be used in subsequent request
+ @realm = auth_header.split(' ')[1]
+ elsif /^Digest/.match(auth_data)
+ creds = auth_data.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
+ key, value = pair.split('=', 2)
+ hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
+ hash
+ end
+ @realm = creds[:realm]
+ @nonce = creds[:nonce]
+ @opaque = creds[:opaque]
+ end
+ end
+
+ return status
+ end
+
# Encode the cookies hash in a format suitable for passing to a
# request.
def encode_cookies
@@ -371,7 +464,7 @@ def generic_url_rewriter
"SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off"
}
- UrlRewriter.new(RackRequest.new(env), {})
+ UrlRewriter.new(Request.new(env), {})
end
def name_with_prefix(prefix, name)
View
22 actionpack/lib/action_controller/layout.rb
@@ -178,17 +178,23 @@ def default_layout(format) #:nodoc:
find_layout(layout, format)
end
+ def layout_list #:nodoc:
+ Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] }
+ end
+
def find_layout(layout, *formats) #:nodoc:
return layout if layout.respond_to?(:render)
view_paths.find_template(layout.to_s =~ /layouts\// ? layout : "layouts/#{layout}", *formats)
+ rescue ActionView::MissingTemplate
+ nil
end
private
def inherited_with_layout(child)
inherited_without_layout(child)
unless child.name.blank?
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
- child.layout(layout_match, {}, true) if child.find_layout(layout_match, :all)
+ child.layout(layout_match, {}, true) unless child.layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty?
end
end
@@ -225,8 +231,16 @@ def active_layout(passed_layout = nil)
private
def candidate_for_layout?(options)
- options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? &&
- !@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action]))
+ template = options[:template] || default_template(options[:action])
+ if options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty?
+ begin
+ !self.view_paths.find_template(template, default_template_format).exempt_from_layout?
+ rescue ActionView::MissingTemplate
+ true
+ end
+ end
+ rescue ActionView::MissingTemplate
+ false
end
def pick_layout(options)
@@ -235,7 +249,7 @@ def pick_layout(options)
when FalseClass
nil
when NilClass, TrueClass
- active_layout if action_has_layout? && !@template.__send__(:_exempt_from_layout?, default_template_name)
+ active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
active_layout(layout)
end
View
21 actionpack/lib/action_controller/middlewares.rb
@@ -0,0 +1,21 @@
+use "ActionController::Lock", :if => lambda {
+ !ActionController::Base.allow_concurrency
+}
+
+use "ActionController::Failsafe"
+
+use "ActiveRecord::QueryCache", :if => lambda { defined?(ActiveRecord) }
+
+["ActionController::Session::CookieStore",
+ "ActionController::Session::MemCacheStore",
+ "ActiveRecord::SessionStore"].each do |store|
+ use(store, ActionController::Base.session_options,
+ :if => lambda {
+ if session_store = ActionController::Base.session_store
+ session_store.name == store
+ end
+ }
+ )
+end
+
+use ActionController::VerbPiggybacking
View
23 actionpack/lib/action_controller/mime_responds.rb
@@ -143,12 +143,27 @@ def any(*args, &block)
custom(@mime_type_priority.first, &block)
end
end
+
+ def self.generate_method_for_mime(mime)
+ sym = mime.is_a?(Symbol) ? mime : mime.to_sym
+ const = sym.to_s.upcase
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{sym}(&block) # def html(&block)
+ custom(Mime::#{const}, &block) # custom(Mime::HTML, &block)
+ end # end
+ RUBY
+ end
- def method_missing(symbol, &block)
- mime_constant = symbol.to_s.upcase
+ Mime::SET.each do |mime|
+ generate_method_for_mime(mime)
+ end
- if Mime::SET.include?(Mime.const_get(mime_constant))
- custom(Mime.const_get(mime_constant), &block)
+ def method_missing(symbol, &block)
+ mime_constant = Mime.const_get(symbol.to_s.upcase)
+
+ if Mime::SET.include?(mime_constant)
+ self.class.generate_method_for_mime(mime_constant)
+ send(symbol, &block)
else
super
end
View
18 actionpack/lib/action_controller/polymorphic_routes.rb
@@ -118,13 +118,17 @@ def polymorphic_path(record_or_hash_or_array, options = {})
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
- def #{action}_polymorphic_url(record_or_hash, options = {})
- polymorphic_url(record_or_hash, options.merge(:action => "#{action}"))
- end
-
- def #{action}_polymorphic_path(record_or_hash, options = {})
- polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path))
- end
+ def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url( # polymorphic_url(
+ record_or_hash, # record_or_hash,
+ options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
+ end # end
+ #
+ def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
+ polymorphic_url( # polymorphic_url(
+ record_or_hash, # record_or_hash,
+ options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
+ end # end
EOT
end
View
73 actionpack/lib/action_controller/rack_process.rb
@@ -1,73 +0,0 @@
-require 'action_controller/cgi_ext'
-
-module ActionController #:nodoc:
- class RackRequest < AbstractRequest #:nodoc:
- attr_accessor :session_options
-
- class SessionFixationAttempt < StandardError #:nodoc:
- end
-
- def initialize(env)
- @env = env
- super()
- end
-
- %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
- PATH_TRANSLATED REMOTE_HOST
- REMOTE_IDENT REMOTE_USER SCRIPT_NAME
- SERVER_NAME SERVER_PROTOCOL
-
- HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
- HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
- HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
- define_method(env.sub(/^HTTP_/n, '').downcase) do
- @env[env]
- end
- end
-
- def query_string
- qs = super
- if !qs.blank?
- qs
- else
- @env['QUERY_STRING']
- end
- end
-
- def body_stream #:nodoc:
- @env['rack.input']
- end
-
- def key?(key)
- @env.key?(key)
- end
-
- def cookies
- Rack::Request.new(@env).cookies
- end
-
- def server_port
- @env['SERVER_PORT'].to_i
- end
-
- def server_software
- @env['SERVER_SOFTWARE'].split("/").first
- end
-
- def session_options
- @env['rack.session.options'] ||= {}
- end
-
- def session_options=(options)
- @env['rack.session.options'] = options
- end
-
- def session
- @env['rack.session'] ||= {}
- end
-
- def reset_session
- @env['rack.session'] = {}
- end
- end
-end
View
512 actionpack/lib/action_controller/request.rb
@@ -3,32 +3,48 @@
require 'strscan'
require 'active_support/memoizable'
+require 'action_controller/cgi_ext'
module ActionController
# CgiRequest and TestRequest provide concrete implementations.
- class AbstractRequest
+ class Request
extend ActiveSupport::Memoizable
- def self.relative_url_root=(relative_url_root)
- ActiveSupport::Deprecation.warn(
- "ActionController::AbstractRequest.relative_url_root= has been renamed." +
- "You can now set it with config.action_controller.relative_url_root=", caller)
- ActionController::Base.relative_url_root=relative_url_root
+ class SessionFixationAttempt < StandardError #:nodoc:
end
- HTTP_METHODS = %w(get head put post delete options)
- HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
-
# The hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }.
attr_reader :env
+ def initialize(env)
+ @env = env
+ end
+
+ %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
+ PATH_TRANSLATED REMOTE_HOST
+ REMOTE_IDENT REMOTE_USER SCRIPT_NAME
+ SERVER_NAME SERVER_PROTOCOL
+
+ HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
+ HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
+ HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
+ define_method(env.sub(/^HTTP_/n, '').downcase) do
+ @env[env]
+ end
+ end
+
+ def key?(key)
+ @env.key?(key)
+ end
+
+ HTTP_METHODS = %w(get head put post delete options)
+ HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
+
# The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
def request_method
method = @env['REQUEST_METHOD']
- method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
-
HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
end
memoize :request_method
@@ -85,7 +101,7 @@ def content_length
# For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_type
- Mime::Type.lookup(content_type_without_parameters)
+ Mime::Type.lookup(parser.content_type_without_parameters)
end
memoize :content_type
@@ -125,15 +141,15 @@ def etag_matches?(etag)
# supplied, both must match, or the request is not considered fresh.
def fresh?(response)
case
- when if_modified_since && if_none_match
- not_modified?(response.last_modified) && etag_matches?(response.etag)
- when if_modified_since
- not_modified?(response.last_modified)
- when if_none_match
- etag_matches?(response.etag)
- else
- false
- end
+ when if_modified_since && if_none_match
+ not_modified?(response.last_modified) && etag_matches?(response.etag)
+ when if_modified_since
+ not_modified?(response.last_modified)
+ when if_none_match
+ etag_matches?(response.etag)
+ else
+ false
+ end
end
# Returns the Mime type for the \format used in the request.
@@ -248,7 +264,6 @@ def server_software
end
memoize :server_software
-
# Returns the complete URL used for this request.
def url
protocol + host_with_port + request_uri
@@ -271,7 +286,7 @@ def raw_host_with_port
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
else
- env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
+ env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end
@@ -332,11 +347,7 @@ def subdomains(tld_length = 1)
# Returns the query string, accounting for server idiosyncrasies.
def query_string
- if uri = @env['REQUEST_URI']
- uri.split('?', 2)[1] || ''
- else
- @env['QUERY_STRING'] || ''
- end
+ @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
end
memoize :query_string
@@ -378,11 +389,7 @@ def path
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
- unless env.include? 'RAW_POST_DATA'
- env['RAW_POST_DATA'] = body.read(content_length)
- body.rewind if body.respond_to?(:rewind)
- end
- env['RAW_POST_DATA']
+ parser.raw_post
end
# Returns both GET and POST \parameters in a single hash.
@@ -391,7 +398,7 @@ def parameters
end
def path_parameters=(parameters) #:nodoc:
- @path_parameters = parameters
+ @env["rack.routing_args"] = parameters
@symbolized_path_parameters = @parameters = nil
end
@@ -407,18 +414,11 @@ def symbolized_path_parameters
#
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
- @path_parameters ||= {}
+ @env["rack.routing_args"] ||= {}
end
- # The request body is an IO input stream. If the RAW_POST_DATA environment
- # variable is already set, wrap it in a StringIO.
def body
- if raw_post = env['RAW_POST_DATA']
- raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
- StringIO.new(raw_post)
- else
- body_stream
- end
+ parser.body
end
def remote_addr
@@ -430,441 +430,53 @@ def referrer
end
alias referer referrer
-
def query_parameters
- @query_parameters ||= self.class.parse_query_parameters(query_string)
+ @query_parameters ||= parser.query_parameters
end
def request_parameters
- @request_parameters ||= parse_formatted_request_parameters
+ @request_parameters ||= parser.request_parameters
end
-
- #--
- # Must be implemented in the concrete request
- #++
-
def body_stream #:nodoc:
+ @env['rack.input']
end
- def cookies #:nodoc:
+ def cookies
+ Rack::Request.new(@env).cookies
end
- def session #:nodoc:
+ def session
+ @env['rack.session'] ||= {}
end
def session=(session) #:nodoc:
@session = session
end
- def reset_session #:nodoc:
+ def reset_session
+ @env['rack.session'] = {}
end
- protected
- # The raw content type string. Use when you need parameters such as
- # charset or boundary which aren't included in the content_type MIME type.
- # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
- def content_type_with_parameters
- content_type_from_legacy_post_data_format_header ||
- env['CONTENT_TYPE'].to_s
- end
-
- # The raw content type string with its parameters stripped off.
- def content_type_without_parameters
- self.class.extract_content_type_without_parameters(content_type_with_parameters)
- end
- memoize :content_type_without_parameters
-
- private
- def content_type_from_legacy_post_data_format_header
- if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
- case x_post_format.to_s.downcase
- when 'yaml'; 'application/x-yaml'
- when 'xml'; 'application/xml'
- end
- end
- end
-
- def parse_formatted_request_parameters
- return {} if content_length.zero?
-
- content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
-
- # Don't parse params for unknown requests.
- return {} if content_type.blank?
-
- mime_type = Mime::Type.lookup(content_type)
- strategy = ActionController::Base.param_parsers[mime_type]
-
- # Only multipart form parsing expects a stream.
- body = (strategy && strategy != :multipart_form) ? raw_post : self.body
-
- case strategy
- when Proc
- strategy.call(body)
- when :url_encoded_form
- self.class.clean_up_ajax_request_body! body
- self.class.parse_query_parameters(body)
- when :multipart_form
- self.class.parse_multipart_form_parameters(body, boundary, content_length, env)
- when :xml_simple, :xml_node
- body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
- when :yaml
- YAML.load(body)
- when :json
- if body.blank?
- {}
- else
- data = ActiveSupport::JSON.decode(body)
- data = {:_json => data} unless data.is_a?(Hash)
- data.with_indifferent_access
- end
- else
- {}
- end
- rescue Exception => e # YAML, XML or Ruby code block errors
- raise
- { "body" => body,
- "content_type" => content_type_with_parameters,
- "content_length" => content_length,
- "exception" => "#{e.message} (#{e.class})",
- "backtrace" => e.backtrace }
- end
-
- def named_host?(host)
- !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
- end
-
- class << self
- def parse_query_parameters(query_string)
- return {} if query_string.blank?
-
- pairs = query_string.split('&').collect do |chunk|
- next if chunk.empty?
- key, value = chunk.split('=', 2)
- next if key.empty?
- value = value.nil? ? nil : CGI.unescape(value)
- [ CGI.unescape(key), value ]
- end.compact
-
- UrlEncodedPairParser.new(pairs).result
- end
-
- def parse_request_parameters(params)
- parser = UrlEncodedPairParser.new
-
- params = params.dup
- until params.empty?
- for key, value in params
- if key.blank?
- params.delete key
- elsif !key.include?('[')
- # much faster to test for the most common case first (GET)
- # and avoid the call to build_deep_hash
- parser.result[key] = get_typed_value(value[0])
- params.delete key
- elsif value.is_a?(Array)
- parser.parse(key, get_typed_value(value.shift))
- params.delete key if value.empty?
- else
- raise TypeError, "Expected array, found #{value.inspect}"
- end
- end
- end
-
- parser.result
- end
-
- def parse_multipart_form_parameters(body, boundary, body_size, env)
- parse_request_parameters(read_multipart(body, boundary, body_size, env))
- end
-
- def extract_multipart_boundary(content_type_with_parameters)
- if content_type_with_parameters =~ MULTIPART_BOUNDARY
- ['multipart/form-data', $1.dup]
- else
- extract_content_type_without_parameters(content_type_with_parameters)
- end
- end
-
- def extract_content_type_without_parameters(content_type_with_parameters)
- $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
- end
-
- def clean_up_ajax_request_body!(body)
- body.chop! if body[-1] == 0
- body.gsub!(/&_=$/, '')
- end
-
-
- private
- def get_typed_value(value)
- case value
- when String
- value
- when NilClass
- ''
- when Array
- value.map { |v| get_typed_value(v) }
- else
- if value.respond_to? :original_filename
- # Uploaded file
- if value.original_filename
- value
- # Multipart param
- else
- result = value.read
- value.rewind
- result
- end
- # Unknown value, neither string nor multipart.
- else
- raise "Unknown form value: #{value.inspect}"
- end
- end
- end
-
- MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
-
- EOL = "\015\012"
-
- def read_multipart(body, boundary, body_size, env)
- params = Hash.new([])
- boundary = "--" + boundary
- quoted_boundary = Regexp.quote(boundary)
- buf = ""
- bufsize = 10 * 1024
- boundary_end=""
-
- # start multipart/form-data
- body.binmode if defined? body.binmode
- case body
- when File
- body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
- when StringIO
- body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
- end
- boundary_size = boundary.size + EOL.size
- body_size -= boundary_size
- status = body.read(boundary_size)
- if nil == status
- raise EOFError, "no content body"
- elsif boundary + EOL != status
- raise EOFError, "bad content body"
- end
-
- loop do
- head = nil
- content =
- if 10240 < body_size
- UploadedTempfile.new("CGI")
- else
- UploadedStringIO.new
- end
- content.binmode if defined? content.binmode
-
- until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
-
- if (not head) and /#{EOL}#{EOL}/n.match(buf)
- buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
- head = $1.dup
- ""
- end
- next
- end
-
- if head and ( (EOL + boundary + EOL).size < buf.size )
- content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
- buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
- end
-
- c = if bufsize < body_size
- body.read(bufsize)
- else
- body.read(body_size)
- end
- if c.nil? || c.empty?
- raise EOFError, "bad content body"
- end
- buf.concat(c)
- body_size -= c.size
- end
-
- buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
- content.print $1
- if "--" == $2
- body_size = -1
- end
- boundary_end = $2.dup
- ""
- end
-
- content.rewind
-
- head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
- if filename = $1 || $2
- if /Mac/ni.match(env['HTTP_USER_AGENT']) and
- /Mozilla/ni.match(env['HTTP_USER_AGENT']) and
- (not /MSIE/ni.match(env['HTTP_USER_AGENT']))
- filename = CGI.unescape(filename)
- end
- content.original_path = filename.dup
- end
-
- head =~ /Content-Type: ([^\r]*)/ni
- content.content_type = $1.dup if $1
-
- head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
- name = $1.dup if $1
-
- if params.has_key?(name)
- params[name].push(content)
- else
- params[name] = [content]
- end
- break if body_size == -1
- end
- raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
-
- begin
- body.rewind if body.respond_to?(:rewind)
- rescue Errno::ESPIPE
- # Handles exceptions raised by input streams that cannot be rewound
- # such as when using plain CGI under Apache
- end
-
- params
- end
+ def session_options
+ @env['rack.session.options'] ||= {}
end
- end
- class UrlEncodedPairParser < StringScanner #:nodoc:
- attr_reader :top, :parent, :result
-
- def initialize(pairs = [])
- super('')
- @result = {}
- pairs.each { |key, value| parse(key, value) }
+ def session_options=(options)
+ @env['rack.session.options'] = options
end
- KEY_REGEXP = %r{([^\[\]=&]+)}
- BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
-
- # Parse the query string
- def parse(key, value)
- self.string = key
- @top, @parent = result, nil
-
- # First scan the bare key
- key = scan(KEY_REGEXP) or return
- key = post_key_check(key)
-
- # Then scan as many nestings as present
- until eos?
- r = scan(BRACKETED_KEY_REGEXP) or return
- key = self[1]
- key = post_key_check(key)
- end
-
- bind(key, value)
+ def server_port
+ @env['SERVER_PORT'].to_i
end
private
- # After we see a key, we must look ahead to determine our next action. Cases:
- #
- # [] follows the key. Then the value must be an array.
- # = follows the key. (A value comes next)
- # & or the end of string follows the key. Then the key is a flag.
- # otherwise, a hash follows the key.
- def post_key_check(key)
- if scan(/\[\]/) # a[b][] indicates that b is an array
- container(key, Array)
- nil
- elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
- container(key, Hash)
- nil
- else # End of key? We do nothing.
- key
- end
- end
-
- # Add a container to the stack.
- def container(key, klass)
- type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
- value = bind(key, klass.new)
- type_conflict! klass, value unless value.is_a?(klass)
- push(value)
- end
-
- # Push a value onto the 'stack', which is actually only the top 2 items.
- def push(value)
- @parent, @top = @top, value
- end
-
- # Bind a key (which may be nil for items in an array) to the provided value.
- def bind(key, value)
- if top.is_a? Array
- if key
- if top[-1].is_a?(Hash) && ! top[-1].key?(key)
- top[-1][key] = value
- else
- top << {key => value}.with_indifferent_access
- push top.last
- value = top[key]
- end
- else
- top << value
- end
- elsif top.is_a? Hash
- key = CGI.unescape(key)
- parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
- top[key] ||= value
- return top[key]
- else
- raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
- end
-
- return value
- end
-
- def type_conflict!(klass, value)
- raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
- end
- end
-
- module UploadedFile
- def self.included(base)
- base.class_eval do
- attr_accessor :original_path, :content_type
- alias_method :local_path, :path
+ def named_host?(host)
+ !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end
- end
- # Take the basename of the upload's original filename.
- # This handles the full Windows paths given by Internet Explorer
- # (and perhaps other broken user agents) without affecting
- # those which give the lone filename.
- # The Windows regexp is adapted from Perl's File::Basename.
- def original_filename
- unless defined? @original_filename
- @original_filename =
- unless original_path.blank?
- if original_path =~ /^(?:.*[:\\\/])?(.*)/m
- $1
- else
- File.basename original_path
- end
- end
+ def parser
+ @parser ||= ActionController::RequestParser.new(@env)
end
- @original_filename
- end
- end
-
- class UploadedStringIO < StringIO
- include UploadedFile
- end
-
- class UploadedTempfile < Tempfile
- include UploadedFile
end
end
View
314 actionpack/lib/action_controller/request_parser.rb
@@ -0,0 +1,314 @@
+module ActionController
+ class RequestParser
+ def initialize(env)
+ @env = env
+ end
+
+ def request_parameters
+ @request_parameters ||= parse_formatted_request_parameters
+ end
+
+ def query_parameters
+ @query_parameters ||= self.class.parse_query_parameters(query_string)
+ end
+
+ # Returns the query string, accounting for server idiosyncrasies.
+ def query_string
+ @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
+ end
+
+ # The request body is an IO input stream. If the RAW_POST_DATA environment
+ # variable is already set, wrap it in a StringIO.
+ def body
+ if raw_post = @env['RAW_POST_DATA']
+ raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
+ StringIO.new(raw_post)
+ else
+ @env['rack.input']
+ end
+ end
+
+ # The raw content type string with its parameters stripped off.
+ def content_type_without_parameters
+ self.class.extract_content_type_without_parameters(content_type_with_parameters)
+ end
+
+ def raw_post
+ unless @env.include? 'RAW_POST_DATA'
+ @env['RAW_POST_DATA'] = body.read(content_length)
+ body.rewind if body.respond_to?(:rewind)
+ end
+ @env['RAW_POST_DATA']
+ end
+
+ private
+
+ def parse_formatted_request_parameters
+ return {} if content_length.zero?
+
+ content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
+
+ # Don't parse params for unknown requests.
+ return {} if content_type.blank?
+
+ mime_type = Mime::Type.lookup(content_type)
+ strategy = ActionController::Base.param_parsers[mime_type]
+
+ # Only multipart form parsing expects a stream.
+ body = (strategy && strategy != :multipart_form) ? raw_post : self.body
+
+ case strategy
+ when Proc
+ strategy.call(body)
+ when :url_encoded_form
+ self.class.clean_up_ajax_request_body! body
+ self.class.parse_query_parameters(body)
+ when :multipart_form
+ self.class.parse_multipart_form_parameters(body, boundary, content_length, @env)
+ when :xml_simple, :xml_node
+ body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
+ when :yaml
+ YAML.load(body)
+ when :json
+ if body.blank?
+ {}
+ else
+ data = ActiveSupport::JSON.decode(body)
+ data = {:_json => data} unless data.is_a?(Hash)
+ data.with_indifferent_access
+ end
+ else
+ {}
+ end
+ rescue Exception => e # YAML, XML or Ruby code block errors
+ raise
+ { "body" => body,
+ "content_type" => content_type_with_parameters,
+ "content_length" => content_length,
+ "exception" => "#{e.message} (#{e.class})",
+ "backtrace" => e.backtrace }
+ end
+
+ def content_length
+ @content_length ||= @env['CONTENT_LENGTH'].to_i
+ end
+
+ # The raw content type string. Use when you need parameters such as
+ # charset or boundary which aren't included in the content_type MIME type.
+ # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
+ def content_type_with_parameters
+ content_type_from_legacy_post_data_format_header || @env['CONTENT_TYPE'].to_s
+ end
+
+ def content_type_from_legacy_post_data_format_header
+ if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
+ case x_post_format.to_s.downcase
+ when 'yaml'; 'application/x-yaml'
+ when 'xml'; 'application/xml'
+ end
+ end
+ end
+
+ class << self
+ def parse_query_parameters(query_string)
+ return {} if query_string.blank?
+
+ pairs = query_string.split('&').collect do |chunk|
+ next if chunk.empty?
+ key, value = chunk.split('=', 2)
+ next if key.empty?
+ value = value.nil? ? nil : CGI.unescape(value)
+ [ CGI.unescape(key), value ]
+ end.compact
+
+ UrlEncodedPairParser.new(pairs).result
+ end
+
+ def parse_request_parameters(params)
+ parser = UrlEncodedPairParser.new
+
+ params = params.dup
+ until params.empty?
+ for key, value in params
+ if key.blank?
+ params.delete key
+ elsif !key.include?('[')
+ # much faster to test for the most common case first (GET)
+ # and avoid the call to build_deep_hash
+ parser.result[key] = get_typed_value(value[0])
+ params.delete key
+ elsif value.is_a?(Array)
+ parser.parse(key, get_typed_value(value.shift))
+ params.delete key if value.empty?
+ else
+ raise TypeError, "Expected array, found #{value.inspect}"
+ end
+ end
+ end
+
+ parser.result
+ end
+
+ def parse_multipart_form_parameters(body, boundary, body_size, env)
+ parse_request_parameters(read_multipart(body, boundary, body_size, env))
+ end
+
+ def extract_multipart_boundary(content_type_with_parameters)
+ if content_type_with_parameters =~ MULTIPART_BOUNDARY
+ ['multipart/form-data', $1.dup]
+ else
+ extract_content_type_without_parameters(content_type_with_parameters)
+ end
+ end
+
+ def extract_content_type_without_parameters(content_type_with_parameters)
+ $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
+ end
+
+ def clean_up_ajax_request_body!(body)
+ body.chop! if body[-1] == 0
+ body.gsub!(/&_=$/, '')
+ end
+
+
+ private
+ def get_typed_value(value)
+ case value
+ when String
+ value
+ when NilClass
+ ''
+ when Array
+ value.map { |v| get_typed_value(v) }
+ else
+ if value.respond_to? :original_filename
+ # Uploaded file
+ if value.original_filename
+ value
+ # Multipart param
+ else
+ result = value.read
+ value.rewind
+ result
+ end
+ # Unknown value, neither string nor multipart.
+ else
+ raise "Unknown form value: #{value.inspect}"
+ end
+ end
+ end
+
+ MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
+
+ EOL = "\015\012"
+
+ def read_multipart(body, boundary, body_size, env)
+ params = Hash.new([])
+ boundary = "--" + boundary
+ quoted_boundary = Regexp.quote(boundary)
+ buf = ""
+ bufsize = 10 * 1024
+ boundary_end=""
+
+ # start multipart/form-data
+ body.binmode if defined? body.binmode
+ case body
+ when File
+ body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
+ when StringIO
+ body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
+ end
+ boundary_size = boundary.size + EOL.size
+ body_size -= boundary_size
+ status = body.read(boundary_size)
+ if nil == status
+ raise EOFError, "no content body"
+ elsif boundary + EOL != status
+ raise EOFError, "bad content body"
+ end
+
+ loop do
+ head = nil
+ content =
+ if 10240 < body_size
+ UploadedTempfile.new("CGI")
+ else
+ UploadedStringIO.new
+ end
+ content.binmode if defined? content.binmode
+
+ until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
+
+