Browse files

Merge commit 'mainstream/master'

  • Loading branch information...
2 parents 2cd8d3b + 07298fd commit 73e9f4e9096515e9f4d97baaa914320c42159985 @lifo lifo committed Dec 26, 2008
Showing with 1,775 additions and 1,184 deletions.
  1. +3 −1 actionmailer/lib/action_mailer/base.rb
  2. +6 −3 actionmailer/test/abstract_unit.rb
  3. +14 −0 actionpack/CHANGELOG
  4. +8 −4 actionpack/lib/action_controller.rb
  5. +1 −1 actionpack/lib/action_controller/assertions/routing_assertions.rb
  6. +46 −23 actionpack/lib/action_controller/base.rb
  7. +1 −1 actionpack/lib/action_controller/benchmarking.rb
  8. +1 −2 actionpack/lib/action_controller/caching.rb
  9. +1 −1 actionpack/lib/action_controller/caching/actions.rb
  10. +1 −1 actionpack/lib/action_controller/caching/pages.rb
  11. +0 −18 actionpack/lib/action_controller/caching/sql_cache.rb
  12. +12 −26 actionpack/lib/action_controller/cookies.rb
  13. +4 −18 actionpack/lib/action_controller/dispatcher.rb
  14. +17 −5 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. +1 −1 actionpack/lib/action_controller/mime_type.rb
  18. +0 −175 actionpack/lib/action_controller/rack_process.rb
  19. +59 −447 actionpack/lib/action_controller/request.rb
  20. +314 −0 actionpack/lib/action_controller/request_parser.rb
  21. +1 −1 actionpack/lib/action_controller/rescue.rb
  22. +113 −55 actionpack/lib/action_controller/response.rb
  23. +13 −5 actionpack/lib/action_controller/session/abstract_store.rb
  24. +5 −7 actionpack/lib/action_controller/session/cookie_store.rb
  25. +12 −3 actionpack/lib/action_controller/streaming.rb
  26. +1 −4 actionpack/lib/action_controller/test_case.rb
  27. +63 −34 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. +0 −11 actionpack/lib/action_view/helpers/prototype_helper.rb
  34. +7 −0 actionpack/lib/action_view/locale/en.yml
  35. +1 −1 actionpack/lib/action_view/partials.rb
  36. +13 −28 actionpack/lib/action_view/paths.rb
  37. +12 −10 actionpack/lib/action_view/renderable.rb
  38. +28 −0 actionpack/lib/action_view/template.rb
  39. +3 −3 actionpack/test/controller/action_pack_assertions_test.rb
  40. +2 −3 actionpack/test/controller/caching_test.rb
  41. +27 −60 actionpack/test/controller/cookie_test.rb
  42. +1 −0 actionpack/test/controller/mime_type_test.rb
  43. +10 −33 actionpack/test/controller/rack_test.rb
  44. +79 −15 actionpack/test/controller/render_test.rb
  45. +39 −41 actionpack/test/controller/request_test.rb
  46. +27 −2 actionpack/test/controller/send_file_test.rb
  47. +25 −1 actionpack/test/controller/session/cookie_store_test.rb
  48. +21 −0 actionpack/test/controller/session/mem_cache_store_test.rb
  49. +11 −0 actionpack/test/template/date_helper_i18n_test.rb
  50. +321 −0 actionpack/test/template/date_helper_test.rb
  51. +1 −1 actionpack/test/template/form_helper_test.rb
  52. +4 −4 activerecord/lib/active_record/associations.rb
  53. +3 −3 activerecord/lib/active_record/associations/has_many_through_association.rb
  54. +9 −4 activerecord/lib/active_record/base.rb
  55. +25 −13 activerecord/lib/active_record/query_cache.rb
  56. +2 −2 activerecord/lib/active_record/timestamp.rb
  57. +0 −1 activerecord/lib/active_record/validations.rb
  58. +2 −2 activerecord/test/cases/associations/eager_test.rb
  59. +13 −0 activerecord/test/cases/associations/has_many_associations_test.rb
  60. +4 −0 activerecord/test/cases/associations/has_many_through_associations_test.rb
  61. +10 −0 activerecord/test/cases/base_test.rb
  62. +18 −0 activerecord/test/cases/method_scoping_test.rb
  63. +3 −1 activerecord/test/cases/reload_models_test.rb
  64. +1 −0 activerecord/test/models/company.rb
  65. +2 −0 activesupport/CHANGELOG
  66. +27 −1 activesupport/lib/active_support/core_ext/module/delegation.rb
  67. +27 −0 activesupport/test/core_ext/module_test.rb
  68. +1 −1 railties/lib/commands/dbconsole.rb
  69. +0 −31 railties/lib/rails/rack/cascade.rb
  70. +22 −13 railties/lib/rails/rack/metal.rb
  71. +2 −2 railties/lib/tasks/tmp.rake
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
12 actionpack/lib/action_controller.rb
@@ -38,11 +38,10 @@ 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'
- autoload :AbstractResponse, 'action_controller/response'
autoload :Base, 'action_controller/base'
autoload :Benchmarking, 'action_controller/benchmarking'
autoload :Caching, 'action_controller/caching'
@@ -60,9 +59,13 @@ 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 :RackResponse, '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'
autoload :Rescue, 'action_controller/rescue'
autoload :Resources, 'action_controller/resources'
@@ -75,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
69 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,
@@ -502,7 +502,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 +859,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 +905,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 +940,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
@@ -990,7 +997,7 @@ def erase_redirect_results #:nodoc:
@performed_redirect = false
response.redirected_to = nil
response.redirected_to_method_params = nil
- response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE
+ response.status = DEFAULT_RENDER_STATUS_CODE
response.headers.delete('Location')
end
@@ -1164,14 +1171,15 @@ 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
def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true
- response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
+ response.status = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
response.body ||= ''
@@ -1185,6 +1193,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 +1211,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 +1259,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 +1315,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
2 actionpack/lib/action_controller/benchmarking.rb
@@ -83,7 +83,7 @@ def perform_action_with_benchmark
end
end
- log_message << " | #{headers["Status"]}"
+ log_message << " | #{response.status}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
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
2 actionpack/lib/action_controller/caching/actions.rb
@@ -113,7 +113,7 @@ def path_options_for(controller, options)
end
def caching_allowed(controller)
- controller.request.get? && controller.response.headers['Status'].to_i == 200
+ controller.request.get? && controller.response.status.to_i == 200
end
def cache_layout?
View
2 actionpack/lib/action_controller/caching/pages.rb
@@ -145,7 +145,7 @@ def cache_page(content = nil, options = nil)
private
def caching_allowed
- request.get? && response.headers['Status'].to_i == 200
+ request.get? && response.status.to_i == 200
end
end
end
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
22 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
@@ -97,8 +83,8 @@ def call(env)
end
def _call(env)
- @request = RackRequest.new(env)
- @response = RackResponse.new
+ @request = Request.new(env)
+ @response = Response.new
dispatch
end
View
22 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
@@ -181,7 +192,7 @@ def redirect?
# - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
# automatically be upcased, with the prefix 'HTTP_' added if needed.
#
- # This method returns an AbstractResponse object, which one can use to
+ # This method returns an Response object, which one can use to
# inspect the details of the response. Furthermore, if this method was
# called from an ActionController::IntegrationTest object, then that
# object's <tt>@response</tt> instance variable will point to the same
@@ -331,9 +342,10 @@ def process(method, path, parameters = nil, headers = nil)
@response = @controller.response
else
# Decorate responses from Rack Middleware and Rails Metal
- # as an AbstractResponse for the purposes of integration testing
- @response = AbstractResponse.new
- @response.headers = @headers.merge('Status' => status.to_s)
+ # as an Response for the purposes of integration testing
+ @response = Response.new
+ @response.status = status.to_s
+ @response.headers.replace(@headers)
@response.body = @body
end
@@ -370,7 +382,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
2 actionpack/lib/action_controller/mime_type.rb
@@ -178,7 +178,7 @@ def ==(mime_type)
def =~(mime_type)
return false if mime_type.blank?
- regexp = Regexp.new(mime_type.to_s)
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
(@synonyms + [ self ]).any? do |synonym|
synonym.to_s =~ regexp
end
View
175 actionpack/lib/action_controller/rack_process.rb
@@ -1,175 +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
-
- class RackResponse < AbstractResponse #:nodoc:
- def initialize
- @writer = lambda { |x| @body << x }
- @block = nil
- super()
- end
-
- # Retrieve status from instance variable if has already been delete
- def status
- @status || super
- end
-
- def to_a(&block)
- @block = block
- @status = headers.delete("Status")
- if [204, 304].include?(status.to_i)
- headers.delete("Content-Type")
- [status, headers.to_hash, []]
- else
- [status, headers.to_hash, self]
- end
- end
-
- def each(&callback)
- if @body.respond_to?(:call)
- @writer = lambda { |x| callback.call(x) }
- @body.call(self, self)
- elsif @body.is_a?(String)
- @body.each_line(&callback)
- else
- @body.each(&callback)
- end
-
- @writer = callback
- @block.call(self) if @block
- end
-
- def write(str)
- @writer.call str.to_s
- str
- end
-
- def close
- @body.close if @body.respond_to?(:close)
- end
-
- def empty?
- @block == nil && @body.empty?
- end
-
- def prepare!
- super
-
- convert_language!
- convert_expires!
- set_status!
- set_cookies!
- end
-
- private
- def convert_language!
- headers["Content-Language"] = headers.delete("language") if headers["language"]
- end
-
- def convert_expires!
- headers["Expires"] = headers.delete("") if headers["expires"]
- end
-
- def convert_content_type!
- super
- headers['Content-Type'] = headers.delete('type') || "text/html"
- headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
- end
-
- def set_content_length!
- super
- headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
- end
-
- def set_status!
- self.status ||= "200 OK"
- end
-
- def set_cookies!
- # Convert 'cookie' header to 'Set-Cookie' headers.
- # Because Set-Cookie header can appear more the once in the response body,
- # we store it in a line break separated string that will be translated to
- # multiple Set-Cookie header by the handler.
- if cookie = headers.delete('cookie')
- cookies = []
-
- case cookie
- when Array then cookie.each { |c| cookies << c.to_s }
- when Hash then cookie.each { |_, c| cookies << c.to_s }
- else cookies << cookie.to_s
- end
-
- headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
- end
- end
- end
-end
View
506 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
@@ -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.
@@ -410,15 +417,8 @@ def path_parameters
@path_parameters ||= {}
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)
+
+ if (not head) and /#{EOL}#{EOL}/n.match(buf)
+ buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
+ head = $1.dup
+ ""
+ end
+ next
+ end
+
+ if head and ( (EOL + boundary + EOL).size < buf.size )
+ content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
+ buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
+ end
+
+ c = if bufsize < body_size
+ body.read(bufsize)
+ else
+ body.read(body_size)
+ end
+ if c.nil? || c.empty?
+ raise EOFError, "bad content body"
+ end
+ buf.concat(c)
+ body_size -= c.size
+ end
+
+ buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
+ content.print $1
+ if "--" == $2
+ body_size = -1
+ end
+ boundary_end = $2.dup
+ ""
+ end
+
+ content.rewind
+
+ head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
+ if filename = $1 || $2
+ if /Mac/ni.match(env['HTTP_USER_AGENT']) and
+ /Mozilla/ni.match(env['HTTP_USER_AGENT']) and
+ (not /MSIE/ni.match(env['HTTP_USER_AGENT']))
+ filename = CGI.unescape(filename)
+ end
+ content.original_path = filename.dup
+ end
+
+ head =~ /Content-Type: ([^\r]*)/ni
+ content.content_type = $1.dup if $1
+
+ head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
+ name = $1.dup if $1
+
+ if params.has_key?(name)
+ params[name].push(content)
+ else
+ params[name] = [content]
+ end
+ break if body_size == -1
+ end
+ raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
+
+ begin
+ body.rewind if body.respond_to?(:rewind)
+ rescue Errno::ESPIPE
+ # Handles exceptions raised by input streams that cannot be rewound
+ # such as when using plain CGI under Apache
+ end
+
+ params
+ end
+ end # class << self
+ end
+end
View
2 actionpack/lib/action_controller/rescue.rb
@@ -102,7 +102,7 @@ def rescue_action_in_public(exception) #:doc:
# doesn't exist, the body of the response will be left empty.
def render_optional_error_file(status_code)
status = interpret_status(status_code)
- path = "#{Rails.public_path}/#{status[0,3]}.html"
+ path = "#{Rails.public_path}/#{status.to_s[0,3]}.html"
if File.exist?(path)
render :file => path, :status => status, :content_type => Mime::HTML
else
View
168 actionpack/lib/action_controller/response.rb
@@ -1,24 +1,25 @@
require 'digest/md5'
module ActionController # :nodoc:
- # Represents an HTTP response generated by a controller action. One can use an
- # ActionController::AbstractResponse object to retrieve the current state of the
- # response, or customize the response. An AbstractResponse object can either
- # represent a "real" HTTP response (i.e. one that is meant to be sent back to the
- # web browser) or a test response (i.e. one that is generated from integration
- # tests). See CgiResponse and TestResponse, respectively.
+ # Represents an HTTP response generated by a controller action. One can use
+ # an ActionController::Response object to retrieve the current state
+ # of the response, or customize the response. An Response object can
+ # either represent a "real" HTTP response (i.e. one that is meant to be sent
+ # back to the web browser) or a test response (i.e. one that is generated
+ # from integration tests). See CgiResponse and TestResponse, respectively.
#
- # AbstractResponse is mostly a Ruby on Rails framework implement detail, and should
- # never be used directly in controllers. Controllers should use the methods defined
- # in ActionController::Base instead. For example, if you want to set the HTTP
- # response's content MIME type, then use ActionControllerBase#headers instead of
- # AbstractResponse#headers.
+ # Response is mostly a Ruby on Rails framework implement detail, and
+ # should never be used directly in controllers. Controllers should use the
+ # methods defined in ActionController::Base instead. For example, if you want
+ # to set the HTTP response's content MIME type, then use
+ # ActionControllerBase#headers instead of Response#headers.
#
- # Nevertheless, integration tests may want to inspect controller responses in more
- # detail, and that's when AbstractResponse can be useful for application developers.
- # Integration test methods such as ActionController::Integration::Session#get and
- # ActionController::Integration::Session#post return objects of type TestResponse
- # (which are of course also of type AbstractResponse).
+ # Nevertheless, integration tests may want to inspect controller responses in
+ # more detail, and that's when Response can be useful for application
+ # developers. Integration test methods such as
+ # ActionController::Integration::Session#get and
+ # ActionController::Integration::Session#post return objects of type
+ # TestResponse (which are of course also of type Response).
#
# For example, the following demo integration "test" prints the body of the
# controller response to the console:
@@ -29,25 +30,25 @@ module ActionController # :nodoc:
# puts @response.body
# end
# end
- class AbstractResponse
+ class Response < Rack::Response
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :request
- # The body content (e.g. HTML) of the response, as a String.
- attr_accessor :body
- # The headers of the response, as a Hash. It maps header names to header values.
- attr_accessor :headers
- attr_accessor :session, :cookies, :assigns, :template, :layout
+ attr_accessor :session, :assigns, :template, :layout
attr_accessor :redirected_to, :redirected_to_method_params
delegate :default_charset, :to => 'ActionController::Base'
def initialize
- @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
- end
+ @status = 200
+ @header = DEFAULT_HEADERS.dup
+
+ @writer = lambda { |x| @body << x }
+ @block = nil
- def status; headers['Status'] end
- def status=(status) headers['Status'] = status end
+ @body = "",
+ @session, @assigns = [], []
+ end
def location; headers['Location'] end
def location=(url) headers['Location'] = url end
@@ -109,11 +110,11 @@ def last_modified=(utc_time)
def etag
headers['ETag']
end
-
+
def etag?
headers.include?('ETag')
end
-
+
def etag=(etag)
if etag.blank?
headers.delete('ETag')
@@ -142,26 +143,77 @@ def prepare!
handle_conditional_get!
set_content_length!
convert_content_type!
+ convert_language!
+ convert_expires!
+ convert_cookies!
+ end
+
+ def each(&callback)
+ if @body.respond_to?(:call)
+ @writer = lambda { |x| callback.call(x) }
+ @body.call(self, self)
+ elsif @body.is_a?(String)
+ @body.each_line(&callback)
+ else
+ @body.each(&callback)
+ end
+
+ @writer = callback
+ @block.call(self) if @block
+ end
+
+ def write(str)
+ @writer.call str.to_s
+ str
+ end
+
+ # Over Rack::Response#set_cookie to add HttpOnly option
+ def set_cookie(key, value)
+ case value
+ when Hash
+ domain = "; domain=" + value[:domain] if value[:domain]
+ path = "; path=" + value[:path] if value[:path]
+ # According to RFC 2109, we need dashes here.
+ # N.B.: cgi.rb uses spaces...
+ expires = "; expires=" + value[:expires].clone.gmtime.
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
+ secure = "; secure" if value[:secure]
+ httponly = "; HttpOnly" if value[:http_only]
+ value = value[:value]
+ end
+ value = [value] unless Array === value
+ cookie = ::Rack::Utils.escape(key) + "=" +
+ value.map { |v| ::Rack::Utils.escape v }.join("&") +
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
+
+ case self["Set-Cookie"]
+ when Array
+ self["Set-Cookie"] << cookie
+ when String
+ self["Set-Cookie"] = [self["Set-Cookie"], cookie]
+ when nil
+ self["Set-Cookie"] = cookie
+ end
end
private
- def handle_conditional_get!
- if etag? || last_modified?
- set_conditional_cache_control!
- elsif nonempty_ok_response?
- self.etag = body
-
- if request && request.etag_matches?(etag)
- self.status = '304 Not Modified'
- self.body = ''
- end
-
- set_conditional_cache_control!
- end
+ def handle_conditional_get!
+ if etag? || last_modified?
+ set_conditional_cache_control!
+ elsif nonempty_ok_response?
+ self.etag = body
+
+ if request && request.etag_matches?(etag)
+ self.status = '304 Not Modified'
+ self.body = ''
+ end
+
+ set_conditional_cache_control!
+ end
end
def nonempty_ok_response?
- ok = !status || status[0..2] == '200'
+ ok = !status || status.to_s[0..2] == '200'
ok && body.is_a?(String) && !body.empty?
end
@@ -172,23 +224,29 @@ def set_conditional_cache_control!
end
def convert_content_type!
- if content_type = headers.delete("Content-Type")
- self.headers["type"] = content_type
- end
- if content_type = headers.delete("Content-type")
- self.headers["type"] = content_type
- end
- if content_type = headers.delete("content-type")
- self.headers["type"] = content_type
- end
+ headers['Content-Type'] ||= "text/html"
+ headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
end
-
- # Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
- # for, say, a 2GB streaming file.
+
+ # Don't set the Content-Length for block-based bodies as that would mean
+ # reading it all into memory. Not nice for, say, a 2GB streaming file.
def set_content_length!
- unless body.respond_to?(:call) || (status && status[0..2] == '304')
+ unless body.respond_to?(:call) || (status && status.to_s[0..2] == '304')
self.headers["Content-Length"] ||= body.size
end
+ headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
+ end
+
+ def convert_language!
+ headers["Content-Language"] = headers.delete("language") if headers["language"]
+ end
+
+ def convert_expires!
+ headers["Expires"] = headers.delete("") if headers["expires"]
+ end
+
+ def convert_cookies!
+ headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact
end
end
end
View
18 actionpack/lib/action_controller/session/abstract_store.rb
@@ -53,6 +53,10 @@ def data
end
private
+ def loaded?
+ @loaded
+ end
+
def load!
@id, session = @by.send(:load_session, @env)
replace(session)
@@ -91,19 +95,23 @@ def initialize(app, options = {})
def call(env)
session = SessionHash.new(self, env)
- original_session = session.dup
env[ENV_SESSION_KEY] = session
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
response = @app.call(env)
- session = env[ENV_SESSION_KEY]
- unless session == original_session
+ session_data = env[ENV_SESSION_KEY]
+ if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?)
options = env[ENV_SESSION_OPTIONS_KEY]
- sid = session.id
- unless set_session(env, sid, session.to_hash)
+ if session_data.is_a?(AbstractStore::SessionHash)
+ sid = session_data.id
+ else
+ sid = generate_sid
+ end
+
+ unless set_session(env, sid, session_data.to_hash)
return response
end
View
12 actionpack/lib/action_controller/session/cookie_store.rb
@@ -89,16 +89,14 @@ def initialize(app, options = {})
end
def call(env)
- session_data = AbstractStore::SessionHash.new(self, env)
- original_value = session_data.dup
-
- env[ENV_SESSION_KEY] = session_data
+ env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
env[ENV_SESSION_OPTIONS_KEY] = @default_options
status, headers, body = @app.call(env)
- unless env[ENV_SESSION_KEY] == original_value
- session_data = marshal(env[ENV_SESSION_KEY].to_hash)
+ session_data = env[ENV_SESSION_KEY]
+ if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?)
+ session_data = marshal(session_data.to_hash)
raise CookieOverflow if session_data.size > MAX
@@ -153,7 +151,7 @@ def load_session(env)
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
- @verifier.generate( persistent_session_id!(session))
+ @verifier.generate(persistent_session_id!(session))
end
# Unmarshal cookie data to a hash and verify its integrity.