diff options
author | Mikel Lindsaar <raasdnil@gmail.com> | 2010-01-19 21:05:37 +1100 |
---|---|---|
committer | Mikel Lindsaar <raasdnil@gmail.com> | 2010-01-19 21:05:37 +1100 |
commit | 2107921000f4a186fed36e676d1ac089c8be1f99 (patch) | |
tree | 42f2188f058f8401d57f0d4c72063689a93e0419 /actionpack | |
parent | ccb7d9def3c20037c9ed5989d8cae1ed68763f4f (diff) | |
parent | ed8501ef16fb2f5e4bd4d987740f5e5f62978400 (diff) | |
download | rails-2107921000f4a186fed36e676d1ac089c8be1f99.tar.gz rails-2107921000f4a186fed36e676d1ac089c8be1f99.tar.bz2 rails-2107921000f4a186fed36e676d1ac089c8be1f99.zip |
Merge branch 'master' of git://github.com/rails/rails into rails
Diffstat (limited to 'actionpack')
47 files changed, 1153 insertions, 983 deletions
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 48725ad82a..3119ee498b 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -62,15 +62,19 @@ module AbstractController # Array[String]:: A list of all methods that should be considered # actions. def action_methods - @action_methods ||= + @action_methods ||= begin # All public instance methods of this class, including ancestors - public_instance_methods(true).map { |m| m.to_s }.to_set - - # Except for public instance methods of Base and its ancestors - internal_methods.map { |m| m.to_s } + - # Be sure to include shadowed public instance methods of this class - public_instance_methods(false).map { |m| m.to_s } - - # And always exclude explicitly hidden actions - hidden_actions + methods = public_instance_methods(true).map { |m| m.to_s }.to_set - + # Except for public instance methods of Base and its ancestors + internal_methods.map { |m| m.to_s } + + # Be sure to include shadowed public instance methods of this class + public_instance_methods(false).map { |m| m.to_s } - + # And always exclude explicitly hidden actions + hidden_actions + + # Clear out AS callback method pollution + methods.reject { |method| method =~ /_one_time_conditions/ } + end end # Returns the full controller name, underscored, without the ending Controller. @@ -116,68 +120,72 @@ module AbstractController self.class.controller_path end - private - # Returns true if the name can be considered an action. This can - # be overridden in subclasses to modify the semantics of what - # can be considered an action. - # - # ==== Parameters - # name<String>:: The name of an action to be tested - # - # ==== Returns - # TrueClass, FalseClass - def action_method?(name) - self.class.action_methods.include?(name) + def action_methods + self.class.action_methods end - # Call the action. Override this in a subclass to modify the - # behavior around processing an action. This, and not #process, - # is the intended way to override action dispatching. - def process_action(method_name, *args) - send_action(method_name, *args) - end + private + # Returns true if the name can be considered an action. This can + # be overridden in subclasses to modify the semantics of what + # can be considered an action. + # + # ==== Parameters + # name<String>:: The name of an action to be tested + # + # ==== Returns + # TrueClass, FalseClass + def action_method?(name) + self.class.action_methods.include?(name) + end - # Actually call the method associated with the action. Override - # this method if you wish to change how action methods are called, - # not to add additional behavior around it. For example, you would - # override #send_action if you want to inject arguments into the - # method. - alias send_action send - - # If the action name was not found, but a method called "action_missing" - # was found, #method_for_action will return "_handle_action_missing". - # This method calls #action_missing with the current action name. - def _handle_action_missing - action_missing(@_action_name) - end + # Call the action. Override this in a subclass to modify the + # behavior around processing an action. This, and not #process, + # is the intended way to override action dispatching. + def process_action(method_name, *args) + send_action(method_name, *args) + end - # Takes an action name and returns the name of the method that will - # handle the action. In normal cases, this method returns the same - # name as it receives. By default, if #method_for_action receives - # a name that is not an action, it will look for an #action_missing - # method and return "_handle_action_missing" if one is found. - # - # Subclasses may override this method to add additional conditions - # that should be considered an action. For instance, an HTTP controller - # with a template matching the action name is considered to exist. - # - # If you override this method to handle additional cases, you may - # also provide a method (like _handle_method_missing) to handle - # the case. - # - # If none of these conditions are true, and method_for_action - # returns nil, an ActionNotFound exception will be raised. - # - # ==== Parameters - # action_name<String>:: An action name to find a method name for - # - # ==== Returns - # String:: The name of the method that handles the action - # nil:: No method name could be found. Raise ActionNotFound. - def method_for_action(action_name) - if action_method?(action_name) then action_name - elsif respond_to?(:action_missing, true) then "_handle_action_missing" + # Actually call the method associated with the action. Override + # this method if you wish to change how action methods are called, + # not to add additional behavior around it. For example, you would + # override #send_action if you want to inject arguments into the + # method. + alias send_action send + + # If the action name was not found, but a method called "action_missing" + # was found, #method_for_action will return "_handle_action_missing". + # This method calls #action_missing with the current action name. + def _handle_action_missing + action_missing(@_action_name) + end + + # Takes an action name and returns the name of the method that will + # handle the action. In normal cases, this method returns the same + # name as it receives. By default, if #method_for_action receives + # a name that is not an action, it will look for an #action_missing + # method and return "_handle_action_missing" if one is found. + # + # Subclasses may override this method to add additional conditions + # that should be considered an action. For instance, an HTTP controller + # with a template matching the action name is considered to exist. + # + # If you override this method to handle additional cases, you may + # also provide a method (like _handle_method_missing) to handle + # the case. + # + # If none of these conditions are true, and method_for_action + # returns nil, an ActionNotFound exception will be raised. + # + # ==== Parameters + # action_name<String>:: An action name to find a method name for + # + # ==== Returns + # String:: The name of the method that handles the action + # nil:: No method name could be found. Raise ActionNotFound. + def method_for_action(action_name) + if action_method?(action_name) then action_name + elsif respond_to?(:action_missing, true) then "_handle_action_missing" + end end - end end end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index d57136dbb8..0dab4a3cc0 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -42,7 +42,7 @@ module AbstractController # Delegates render_to_body and sticks the result in self.response_body. def render(*args) if response_body - raise AbstractController::DoubleRenderError, "OMG" + raise AbstractController::DoubleRenderError, "Can only render or redirect once per action" end self.response_body = render_to_body(*args) diff --git a/actionpack/lib/action_controller/deprecated/dispatcher.rb b/actionpack/lib/action_controller/deprecated/dispatcher.rb index 3da3c8ce7d..8c21e375dd 100644 --- a/actionpack/lib/action_controller/deprecated/dispatcher.rb +++ b/actionpack/lib/action_controller/deprecated/dispatcher.rb @@ -1,8 +1,5 @@ module ActionController class Dispatcher - cattr_accessor :prepare_each_request - self.prepare_each_request = false - class << self def before_dispatch(*args, &block) ActiveSupport::Deprecation.warn "ActionController::Dispatcher.before_dispatch is deprecated. " << @@ -18,7 +15,7 @@ module ActionController def to_prepare(*args, &block) ActiveSupport::Deprecation.warn "ActionController::Dispatcher.to_prepare is deprecated. " << - "Please use ActionDispatch::Callbacks.to_prepare instead.", caller + "Please use config.to_prepare instead", caller ActionDispatch::Callbacks.after(*args, &block) end diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index 5b51bd21d0..7aa687b52c 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -1,48 +1,4 @@ module ActionController #:nodoc: - # Cookies are read and written through ActionController#cookies. - # - # The cookies being read are the ones received along with the request, the cookies - # being written will be sent out with the response. Reading a cookie does not get - # the cookie object itself back, just the value it holds. - # - # Examples for writing: - # - # # Sets a simple session cookie. - # cookies[:user_name] = "david" - # - # # Sets a cookie that expires in 1 hour. - # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } - # - # Examples for reading: - # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # - # Example for deleting: - # - # cookies.delete :user_name - # - # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie: - # - # cookies[:key] = { - # :value => 'a yummy cookie', - # :expires => 1.year.from_now, - # :domain => 'domain.com' - # } - # - # cookies.delete(:key, :domain => 'domain.com') - # - # The option symbols for setting cookies are: - # - # * <tt>:value</tt> - The cookie's value or list of values (as an array). - # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root - # of the application. - # * <tt>:domain</tt> - The domain for which this cookie applies. - # * <tt>:expires</tt> - The time at which this cookie expires, as a Time object. - # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers. - # Default is +false+. - # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or - # only HTTP. Defaults to +false+. module Cookies extend ActiveSupport::Concern @@ -52,146 +8,10 @@ module ActionController #:nodoc: helper_method :cookies cattr_accessor :cookie_verifier_secret end - - protected - # Returns the cookie container, which operates as described above. + + private def cookies - @cookies ||= CookieJar.build(request, response) - end - end - - class CookieJar < Hash #:nodoc: - def self.build(request, response) - new.tap do |hash| - hash.update(request.cookies) - hash.response = response - end - end - - attr_accessor :response - - # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. - def [](name) - 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 []=(key, options) - if options.is_a?(Hash) - options.symbolize_keys! - value = options[:value] - else - value = options - options = { :value => value } + request.cookie_jar end - - super(key.to_s, value) - - options[:path] ||= "/" - 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(key, options = {}) - options.symbolize_keys! - options[:path] ||= "/" - value = super(key.to_s) - response.delete_cookie(key, options) - value - end - - # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: - # - # cookies.permanent[:prefers_open_id] = true - # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT - # - # This jar is only meant for writing. You'll read permanent cookies through the regular accessor. - # - # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: - # - # cookies.permanent.signed[:remember_me] = current_user.id - # # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT - def permanent - @permanent ||= PermanentCookieJar.new(self) - end - - # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from - # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed - # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will - # be raised. - # - # This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret. - # - # Example: - # - # cookies.signed[:discount] = 45 - # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/ - # - # cookies.signed[:discount] # => 45 - def signed - @signed ||= SignedCookieJar.new(self) - end - end - - class PermanentCookieJar < CookieJar #:nodoc: - def initialize(parent_jar) - @parent_jar = parent_jar - end - - def []=(key, options) - if options.is_a?(Hash) - options.symbolize_keys! - else - options = { :value => options } - end - - options[:expires] = 20.years.from_now - @parent_jar[key] = options - end - - def signed - @signed ||= SignedCookieJar.new(self) - end - - def controller - @parent_jar.controller - end - - def method_missing(method, *arguments, &block) - @parent_jar.send(method, *arguments, &block) - end - end - - class SignedCookieJar < CookieJar #:nodoc: - def initialize(parent_jar) - unless ActionController::Base.cookie_verifier_secret - raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies" - end - - @parent_jar = parent_jar - @verifier = ActiveSupport::MessageVerifier.new(ActionController::Base.cookie_verifier_secret) - end - - def [](name) - @verifier.verify(@parent_jar[name]) - end - - def []=(key, options) - if options.is_a?(Hash) - options.symbolize_keys! - options[:value] = @verifier.generate(options[:value]) - else - options = { :value => @verifier.generate(options) } - end - - @parent_jar[key] = options - end - - def method_missing(method, *arguments, &block) - @parent_jar.send(method, *arguments, &block) - end end end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 7222b7b2fa..876f778751 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -20,11 +20,7 @@ module ActionController result = super payload[:controller] = self.class.name payload[:action] = self.action_name - payload[:formats] = request.formats.map(&:to_s) - payload[:remote_ip] = request.remote_ip - payload[:method] = request.method payload[:status] = response.status - payload[:request_uri] = request.request_uri rescue "unknown" append_info_to_payload(payload) result end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index cb0e600871..6178a59029 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -1,7 +1,7 @@ module ActionController #:nodoc: - # Responder is responsible to expose a resource for different mime requests, + # Responder is responsible for exposing a resource to different mime requests, # usually depending on the HTTP verb. The responder is triggered when - # respond_with is called. The simplest case to study is a GET request: + # <code>respond_with</code> is called. The simplest case to study is a GET request: # # class PeopleController < ApplicationController # respond_to :html, :xml, :json @@ -12,17 +12,17 @@ module ActionController #:nodoc: # end # end # - # When a request comes, for example with format :xml, three steps happen: + # When a request comes in, for example for an XML response, three steps happen: # - # 1) responder searches for a template at people/index.xml; + # 1) the responder searches for a template at people/index.xml; # - # 2) if the template is not available, it will invoke :to_xml in the given resource; + # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource; # - # 3) if the responder does not respond_to :to_xml, call :to_format on it. + # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. # # === Builtin HTTP verb semantics # - # Rails default responder holds semantics for each HTTP verb. Depending on the + # The default Rails responder holds semantics for each HTTP verb. Depending on the # content type, verb and the resource status, it will behave differently. # # Using Rails default responder, a POST request for creating an object could @@ -55,7 +55,7 @@ module ActionController #:nodoc: # # === Nested resources # - # You can given nested resource as you do in form_for and polymorphic_url. + # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>. # Consider the project has many tasks example. The create action for # TasksController would be like: # @@ -67,15 +67,15 @@ module ActionController #:nodoc: # end # # Giving an array of resources, you ensure that the responder will redirect to - # project_task_url instead of task_url. + # <code>project_task_url</code> instead of <code>task_url</code>. # - # Namespaced and singleton resources requires a symbol to be given, as in + # Namespaced and singleton resources require a symbol to be given, as in # polymorphic urls. If a project has one manager which has many tasks, it # should be invoked as: # # respond_with(@project, :manager, @task) # - # Check polymorphic_url documentation for more examples. + # Check <code>polymorphic_url</code> documentation for more examples. # class Responder attr_reader :controller, :request, :format, :resource, :resources, :options @@ -126,7 +126,7 @@ module ActionController #:nodoc: navigation_behavior(e) end - # All others formats follow the procedure below. First we try to render a + # All other formats follow the procedure below. First we try to render a # template, if the template is not available, we verify if the resource # responds to :to_format and display it. # @@ -183,11 +183,11 @@ module ActionController #:nodoc: @default_response.call end - # display is just a shortcut to render a resource with the current format. + # Display is just a shortcut to render a resource with the current format. # # display @user, :status => :ok # - # For xml request is equivalent to: + # For XML requests it's equivalent to: # # render :xml => @user, :status => :ok # @@ -204,14 +204,14 @@ module ActionController #:nodoc: controller.render given_options.merge!(options).merge!(format => resource) end - # Check if the resource has errors or not. + # Check whether the resource has errors. # def has_errors? resource.respond_to?(:errors) && !resource.errors.empty? end - # By default, render the :edit action for html requests with failure, unless - # the verb is post. + # By default, render the <code>:edit</code> action for HTML requests with failure, unless + # the verb is POST. # def default_action @action ||= ACTIONS_FOR_VERBS[request.method] diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index c193a5eff4..d62269b9af 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -10,6 +10,9 @@ module ActionController @_response = response @_response.request = request ret = process(request.parameters[:action]) + if cookies = @_request.env['action_dispatch.cookies'] + cookies.write(@_response) + end @_response.body ||= self.response_body @_response.prepare! set_test_assigns diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 741101a210..7ea64c1923 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -23,7 +23,6 @@ module ActionController initializer "action_controller.initialize_routing" do |app| app.route_configuration_files << app.config.routes_configuration_file app.route_configuration_files << app.config.builtin_routes_configuration_file - app.reload_routes! end initializer "action_controller.initialize_framework_caches" do @@ -40,54 +39,5 @@ module ActionController ActionController::Base.view_paths = view_path if ActionController::Base.view_paths.blank? end - class MetalMiddlewareBuilder - def initialize(metals) - @metals = metals - end - - def new(app) - ActionDispatch::Cascade.new(@metals, app) - end - - def name - ActionDispatch::Cascade.name - end - alias_method :to_s, :name - end - - initializer "action_controller.initialize_metal" do |app| - metal_root = "#{Rails.root}/app/metal" - load_list = app.config.metals || Dir["#{metal_root}/**/*.rb"] - - metals = load_list.map { |metal| - metal = File.basename(metal.gsub("#{metal_root}/", ''), '.rb') - require_dependency metal - metal.camelize.constantize - }.compact - - middleware = MetalMiddlewareBuilder.new(metals) - app.config.middleware.insert_before(:"ActionDispatch::ParamsParser", middleware) - end - - # Prepare dispatcher callbacks and run 'prepare' callbacks - initializer "action_controller.prepare_dispatcher" do |app| - # TODO: This used to say unless defined?(Dispatcher). Find out why and fix. - # Notice that at this point, ActionDispatch::Callbacks were already loaded. - require 'rails/dispatcher' - ActionController::Dispatcher.prepare_each_request = true unless app.config.cache_classes - - unless app.config.cache_classes - # Setup dev mode route reloading - routes_last_modified = app.routes_changed_at - reload_routes = lambda do - unless app.routes_changed_at == routes_last_modified - routes_last_modified = app.routes_changed_at - app.reload_routes! - end - end - ActionDispatch::Callbacks.before { |callbacks| reload_routes.call } - end - end - end end diff --git a/actionpack/lib/action_controller/railties/subscriber.rb b/actionpack/lib/action_controller/railties/subscriber.rb index a9f5d16c58..6659e5df47 100644 --- a/actionpack/lib/action_controller/railties/subscriber.rb +++ b/actionpack/lib/action_controller/railties/subscriber.rb @@ -3,18 +3,13 @@ module ActionController class Subscriber < Rails::Subscriber def process_action(event) payload = event.payload - - info "\nProcessed #{payload[:controller]}##{payload[:action]} " \ - "to #{payload[:formats].join(', ')} (for #{payload[:remote_ip]} at #{event.time.to_s(:db)}) " \ - "[#{payload[:method].to_s.upcase}]" - info " Parameters: #{payload[:params].inspect}" unless payload[:params].blank? additions = ActionController::Base.log_process_action(payload) message = "Completed in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? - message << " | #{payload[:status]} [#{payload[:request_uri]}]\n\n" + message << " by #{payload[:controller]}##{payload[:action]} [#{payload[:status]}]" info(message) end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 71bb67d3ff..082562d921 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -43,8 +43,10 @@ module ActionDispatch autoload_under 'middleware' do autoload :Callbacks autoload :Cascade + autoload :Cookies autoload :Flash autoload :Head + autoload :Notifications autoload :ParamsParser autoload :Rescue autoload :ShowExceptions @@ -55,7 +57,15 @@ module ActionDispatch autoload :Routing module Http - autoload :Headers, 'action_dispatch/http/headers' + extend ActiveSupport::Autoload + + autoload :Cache + autoload :Headers + autoload :MimeNegotiation + autoload :Parameters + autoload :Upload + autoload :UploadedFile, 'action_dispatch/http/upload' + autoload :URL end module Session diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb new file mode 100644 index 0000000000..428e62dc6b --- /dev/null +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -0,0 +1,123 @@ +module ActionDispatch + module Http + module Cache + module Request + def if_modified_since + if since = env['HTTP_IF_MODIFIED_SINCE'] + Time.rfc2822(since) rescue nil + end + end + + def if_none_match + env['HTTP_IF_NONE_MATCH'] + end + + def not_modified?(modified_at) + if_modified_since && modified_at && if_modified_since >= modified_at + end + + def etag_matches?(etag) + if_none_match && if_none_match == etag + end + + # Check response freshness (Last-Modified and ETag) against request + # If-Modified-Since and If-None-Match conditions. If both headers are + # supplied, both must match, or the request is not considered fresh. + def fresh?(response) + last_modified = if_modified_since + etag = if_none_match + + return false unless last_modified || etag + + success = true + success &&= not_modified?(response.last_modified) if last_modified + success &&= etag_matches?(response.etag) if etag + success + end + end + + module Response + def cache_control + @cache_control ||= {} + end + + def last_modified + if last = headers['Last-Modified'] + Time.httpdate(last) + end + end + + def last_modified? + headers.include?('Last-Modified') + end + + def last_modified=(utc_time) + headers['Last-Modified'] = utc_time.httpdate + end + + def etag + @etag + end + + def etag? + @etag + end + + def etag=(etag) + key = ActiveSupport::Cache.expand_cache_key(etag) + @etag = %("#{Digest::MD5.hexdigest(key)}") + end + + private + + def handle_conditional_get! + if etag? || last_modified? || !@cache_control.empty? + set_conditional_cache_control! + elsif nonempty_ok_response? + self.etag = @body + + if request && request.etag_matches?(etag) + self.status = 304 + self.body = [] + end + + set_conditional_cache_control! + else + headers["Cache-Control"] = "no-cache" + end + end + + def nonempty_ok_response? + @status == 200 && string_body? + end + + def string_body? + !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) } + end + + DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" + + def set_conditional_cache_control! + control = @cache_control + + if control.empty? + headers["Cache-Control"] = DEFAULT_CACHE_CONTROL + elsif @cache_control[:no_cache] + headers["Cache-Control"] = "no-cache" + else + extras = control[:extras] + max_age = control[:max_age] + + options = [] + options << "max-age=#{max_age.to_i}" if max_age + options << (control[:public] ? "public" : "private") + options << "must-revalidate" if control[:must_revalidate] + options.concat(extras) if extras + + headers["Cache-Control"] = options.join(", ") + end + end + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb new file mode 100644 index 0000000000..40617e239a --- /dev/null +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -0,0 +1,101 @@ +module ActionDispatch + module Http + module MimeNegotiation + # The MIME type of the HTTP request, such as Mime::XML. + # + # For backward compatibility, the post \format is extracted from the + # X-Post-Data-Format HTTP header if present. + def content_type + @env["action_dispatch.request.content_type"] ||= begin + if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + Mime::Type.lookup($1.strip.downcase) + else + nil + end + end + end + + # Returns the accepted MIME type for the request. + def accepts + @env["action_dispatch.request.accepts"] ||= begin + header = @env['HTTP_ACCEPT'].to_s.strip + + if header.empty? + [content_type] + else + Mime::Type.parse(header) + end + end + end + + # Returns the Mime type for the \format used in the request. + # + # GET /posts/5.xml | request.format => Mime::XML + # GET /posts/5.xhtml | request.format => Mime::HTML + # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt> + # + def format(view_path = []) + formats.first + end + + def formats + accept = @env['HTTP_ACCEPT'] + + @env["action_dispatch.request.formats"] ||= + if parameters[:format] + Array(Mime[parameters[:format]]) + elsif xhr? || (accept && !accept.include?(?,)) + accepts + else + [Mime::HTML] + end + end + + # Sets the \format by string extension, which can be used to force custom formats + # that are not controlled by the extension. + # + # class ApplicationController < ActionController::Base + # before_filter :adjust_format_for_iphone + # + # private + # def adjust_format_for_iphone + # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] + # end + # end + def format=(extension) + parameters[:format] = extension.to_s + @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] + end + + # Returns a symbolized version of the <tt>:format</tt> parameter of the request. + # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt> + # otherwise. + def template_format + parameter_format = parameters[:format] + + if parameter_format + parameter_format + elsif xhr? + :js + else + :html + end + end + + # Receives an array of mimes and return the first user sent mime that + # matches the order array. + # + def negotiate_mime(order) + formats.each do |priority| + if priority == Mime::ALL + return order.first + elsif order.include?(priority) + return priority + end + end + + order.include?(Mime::ALL) ? formats.first : nil + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb new file mode 100644 index 0000000000..97546d5f93 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -0,0 +1,50 @@ +require 'active_support/core_ext/hash/keys' + +module ActionDispatch + module Http + module Parameters + # Returns both GET and POST \parameters in a single hash. + def parameters + @env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access + end + alias :params :parameters + + def path_parameters=(parameters) #:nodoc: + @env.delete("action_dispatch.request.symbolized_path_parameters") + @env.delete("action_dispatch.request.parameters") + @env["action_dispatch.request.path_parameters"] = parameters + end + + # The same as <tt>path_parameters</tt> with explicitly symbolized keys. + def symbolized_path_parameters + @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys + end + + # Returns a hash with the \parameters used to form the \path of the request. + # Returned hash keys are strings: + # + # {'action' => 'my_action', 'controller' => 'my_controller'} + # + # See <tt>symbolized_path_parameters</tt> for symbolized keys. + def path_parameters + @env["action_dispatch.request.path_parameters"] ||= {} + end + + private + + # Convert nested Hashs to HashWithIndifferentAccess + def normalize_parameters(value) + case value + when Hash + h = {} + value.each { |k, v| h[k] = normalize_parameters(v) } + h.with_indifferent_access + when Array + value.map { |e| normalize_parameters(e) } + else + value + end + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 22a08ec10d..187ce7c15d 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -2,14 +2,17 @@ require 'tempfile' require 'stringio' require 'strscan' -require 'active_support/memoizable' -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' require 'action_dispatch/http/headers' module ActionDispatch class Request < Rack::Request + include ActionDispatch::Http::Cache::Request + include ActionDispatch::Http::MimeNegotiation + include ActionDispatch::Http::Parameters + include ActionDispatch::Http::Upload + include ActionDispatch::Http::URL %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -19,9 +22,11 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA ].each do |env| - define_method(env.sub(/^HTTP_/n, '').downcase) do - @env[env] - end + class_eval <<-METHOD, __FILE__, __LINE__ + 1 + def #{env.sub(/^HTTP_/n, '').downcase} + @env["#{env}"] + end + METHOD end def key?(key) @@ -81,25 +86,6 @@ module ActionDispatch Http::Headers.new(@env) end - # Returns the content length of the request as an integer. - def content_length - super.to_i - end - - # The MIME type of the HTTP request, such as Mime::XML. - # - # For backward compatibility, the post \format is extracted from the - # X-Post-Data-Format HTTP header if present. - def content_type - @env["action_dispatch.request.content_type"] ||= begin - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ - Mime::Type.lookup($1.strip.downcase) - else - nil - end - end - end - def forgery_whitelisted? method == :get || xhr? || content_type.nil? || !content_type.verify_request? end @@ -108,104 +94,9 @@ module ActionDispatch content_type.to_s end - # Returns the accepted MIME type for the request. - def accepts - @env["action_dispatch.request.accepts"] ||= begin - header = @env['HTTP_ACCEPT'].to_s.strip - - if header.empty? - [content_type] - else - Mime::Type.parse(header) - end - end - end - - def if_modified_since - if since = env['HTTP_IF_MODIFIED_SINCE'] - Time.rfc2822(since) rescue nil - end - end - - def if_none_match - env['HTTP_IF_NONE_MATCH'] - end - - def not_modified?(modified_at) - if_modified_since && modified_at && if_modified_since >= modified_at - end - - def etag_matches?(etag) - if_none_match && if_none_match == etag - end - - # Check response freshness (Last-Modified and ETag) against request - # If-Modified-Since and If-None-Match conditions. If both headers are - # supplied, both must match, or the request is not considered fresh. - def fresh?(response) - last_modified = if_modified_since - etag = if_none_match - - return false unless last_modified || etag - - success = true - success &&= not_modified?(response.last_modified) if last_modified - success &&= etag_matches?(response.etag) if etag - success - end - - # Returns the Mime type for the \format used in the request. - # - # GET /posts/5.xml | request.format => Mime::XML - # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt> - # - def format(view_path = []) - formats.first - end - - def formats - accept = @env['HTTP_ACCEPT'] - - @env["action_dispatch.request.formats"] ||= - if parameters[:format] - Array.wrap(Mime[parameters[:format]]) - elsif xhr? || (accept && !accept.include?(?,)) - accepts - else - [Mime::HTML] - end - end - - # Sets the \format by string extension, which can be used to force custom formats - # that are not controlled by the extension. - # - # class ApplicationController < ActionController::Base - # before_filter :adjust_format_for_iphone - # - # private - # def adjust_format_for_iphone - # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] - # end - # end - def format=(extension) - parameters[:format] = extension.to_s - @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])] - end - - # Returns a symbolized version of the <tt>:format</tt> parameter of the request. - # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt> - # otherwise. - def template_format - parameter_format = parameters[:format] - - if parameter_format - parameter_format - elsif xhr? - :js - else - :html - end + # Returns the content length of the request as an integer. + def content_length + super.to_i end # Returns true if the request's "X-Requested-With" header contains @@ -238,7 +129,7 @@ module ActionDispatch if @env.include? 'HTTP_CLIENT_IP' if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP']) # We don't know which came from the proxy, and which from the user - raise ActionController::ActionControllerError.new(<<EOM) + raise ActionController::ActionControllerError.new <<EOM IP spoofing attack?! HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect} @@ -264,124 +155,6 @@ EOM (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil end - # Returns the complete URL used for this request. - def url - protocol + host_with_port + request_uri - end - - # Returns 'https://' if this is an SSL request and 'http://' otherwise. - def protocol - ssl? ? 'https://' : 'http://' - end - - # Is this an SSL request? - def ssl? - @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' - end - - # Returns the \host for this request, such as "example.com". - def raw_host_with_port - if forwarded = env["HTTP_X_FORWARDED_HOST"] - forwarded.split(/,\s?/).last - else - env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" - end - end - - # Returns the host for this request, such as example.com. - def host - raw_host_with_port.sub(/:\d+$/, '') - end - - # Returns a \host:\port string for this request, such as "example.com" or - # "example.com:8080". - def host_with_port - "#{host}#{port_string}" - end - - # Returns the port number of this request as an integer. - def port - if raw_host_with_port =~ /:(\d+)$/ - $1.to_i - else - standard_port - end - end - - # Returns the standard \port number for this request's protocol. - def standard_port - case protocol - when 'https://' then 443 - else 80 - end - end - - # Returns a \port suffix like ":8080" if the \port number of this request - # is not the default HTTP \port 80 or HTTPS \port 443. - def port_string - port == standard_port ? '' : ":#{port}" - end - - def server_port - @env['SERVER_PORT'].to_i - end - - # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify - # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". - def domain(tld_length = 1) - return nil unless named_host?(host) - - host.split('.').last(1 + tld_length).join('.') - end - - # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be - # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, - # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> - # in "www.rubyonrails.co.uk". - def subdomains(tld_length = 1) - return [] unless named_host?(host) - parts = host.split('.') - parts[0..-(tld_length+2)] - end - - # Returns the query string, accounting for server idiosyncrasies. - def query_string - @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '') - end - - # Returns the request URI, accounting for server idiosyncrasies. - # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. - def request_uri - if uri = @env['REQUEST_URI'] - # Remove domain, which webrick puts into the request_uri. - (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri - else - # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. - uri = @env['PATH_INFO'].to_s - - if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) - uri = uri.sub(/#{script_filename}\//, '') - end - - env_qs = @env['QUERY_STRING'].to_s - uri += "?#{env_qs}" unless env_qs.empty? - - if uri.blank? - @env.delete('REQUEST_URI') - else - @env['REQUEST_URI'] = uri - end - end - end - - # Returns the interpreted \path to requested resource after all the installation - # directory of this application was taken into account. - def path - path = request_uri.to_s[/\A[^\?]*/] - path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') - path - end - # Read the request \body. This is useful for web services that need to # work with raw requests directly. def raw_post @@ -392,33 +165,6 @@ EOM @env['RAW_POST_DATA'] end - # Returns both GET and POST \parameters in a single hash. - def parameters - @env["action_dispatch.request.parameters"] ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access - end - alias_method :params, :parameters - - def path_parameters=(parameters) #:nodoc: - @env.delete("action_dispatch.request.symbolized_path_parameters") - @env.delete("action_dispatch.request.parameters") - @env["action_dispatch.request.path_parameters"] = parameters - end - - # The same as <tt>path_parameters</tt> with explicitly symbolized keys. - def symbolized_path_parameters - @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys - end - - # Returns a hash with the \parameters used to form the \path of the request. - # Returned hash keys are strings: - # - # {'action' => 'my_action', 'controller' => 'my_controller'} - # - # See <tt>symbolized_path_parameters</tt> for symbolized keys. - def path_parameters - @env["action_dispatch.request.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 @@ -434,18 +180,6 @@ EOM FORM_DATA_MEDIA_TYPES.include?(content_type.to_s) end - # Override Rack's GET method to support indifferent access - def GET - @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) - end - alias_method :query_parameters, :GET - - # Override Rack's POST method to support indifferent access - def POST - @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super) - end - alias_method :request_parameters, :POST - def body_stream #:nodoc: @env['rack.input'] end @@ -463,6 +197,19 @@ EOM @env['rack.session.options'] = options end + # Override Rack's GET method to support indifferent access + def GET + @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) + end + alias :query_parameters :GET + + # Override Rack's POST method to support indifferent access + def POST + @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super) + end + alias :request_parameters :POST + + # Returns the authorization header regardless of whether it was specified directly or through one of the # proxy alternatives. def authorization @@ -471,77 +218,5 @@ EOM @env['X_HTTP_AUTHORIZATION'] || @env['REDIRECT_X_HTTP_AUTHORIZATION'] end - - # Receives an array of mimes and return the first user sent mime that - # matches the order array. - # - def negotiate_mime(order) - formats.each do |priority| - if priority == Mime::ALL - return order.first - elsif order.include?(priority) - return priority - end - end - - order.include?(Mime::ALL) ? formats.first : nil - end - - private - - def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) - end - - module UploadedFile - def self.extended(object) - object.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path if method_defined?(:path) - end - end - - # Take the basename of the upload's original filename. - # This handles the full Windows paths given by Internet Explorer - # (and perhaps other broken user agents) without affecting - # those which give the lone filename. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - unless defined? @original_filename - @original_filename = - unless original_path.blank? - if original_path =~ /^(?:.*[:\\\/])?(.*)/m - $1 - else - File.basename original_path - end - end - end - @original_filename - end - end - - # Convert nested Hashs to HashWithIndifferentAccess and replace - # file upload hashs with UploadedFile objects - def normalize_parameters(value) - case value - when Hash - if value.has_key?(:tempfile) - upload = value[:tempfile] - upload.extend(UploadedFile) - upload.original_path = value[:filename] - upload.content_type = value[:type] - upload - else - h = {} - value.each { |k, v| h[k] = normalize_parameters(v) } - h.with_indifferent_access - end - when Array - value.map { |e| normalize_parameters(e) } - else - value - end - end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 8524bbd993..f299306ff4 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -32,6 +32,8 @@ module ActionDispatch # :nodoc: # end # end class Response < Rack::Response + include ActionDispatch::Http::Cache::Response + attr_accessor :request, :blank attr_writer :header, :sending_file @@ -55,10 +57,6 @@ module ActionDispatch # :nodoc: yield self if block_given? end - def cache_control - @cache_control ||= {} - end - def status=(status) @status = Rack::Utils.status_code(status) end @@ -114,33 +112,6 @@ module ActionDispatch # :nodoc: # information. attr_accessor :charset, :content_type - def last_modified - if last = headers['Last-Modified'] - Time.httpdate(last) - end - end - - def last_modified? - headers.include?('Last-Modified') - end - - def last_modified=(utc_time) - headers['Last-Modified'] = utc_time.httpdate - end - - def etag - @etag - end - - def etag? - @etag - end - - def etag=(etag) - key = ActiveSupport::Cache.expand_cache_key(etag) - @etag = %("#{Digest::MD5.hexdigest(key)}") - end - CONTENT_TYPE = "Content-Type" cattr_accessor(:default_charset) { "utf-8" } @@ -148,7 +119,7 @@ module ActionDispatch # :nodoc: def to_a assign_default_content_type_and_charset! handle_conditional_get! - self["Set-Cookie"] = @cookie.join("\n") + self["Set-Cookie"] = @cookie.join("\n") unless @cookie.blank? self["ETag"] = @etag if @etag super end @@ -222,31 +193,6 @@ module ActionDispatch # :nodoc: end private - def handle_conditional_get! - if etag? || last_modified? || !@cache_control.empty? - set_conditional_cache_control! - elsif nonempty_ok_response? - self.etag = @body - - if request && request.etag_matches?(etag) - self.status = 304 - self.body = [] - end - - set_conditional_cache_control! - else - headers["Cache-Control"] = "no-cache" - end - end - - def nonempty_ok_response? - @status == 200 && string_body? - end - - def string_body? - !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) } - end - def assign_default_content_type_and_charset! return if headers[CONTENT_TYPE].present? @@ -259,27 +205,5 @@ module ActionDispatch # :nodoc: headers[CONTENT_TYPE] = type end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" - - def set_conditional_cache_control! - control = @cache_control - - if control.empty? - headers["Cache-Control"] = DEFAULT_CACHE_CONTROL - elsif @cache_control[:no_cache] - headers["Cache-Control"] = "no-cache" - else - extras = control[:extras] - max_age = control[:max_age] - - options = [] - options << "max-age=#{max_age.to_i}" if max_age - options << (control[:public] ? "public" : "private") - options << "must-revalidate" if control[:must_revalidate] - options.concat(extras) if extras - - headers["Cache-Control"] = options.join(", ") - end - end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb new file mode 100644 index 0000000000..dc6121b911 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -0,0 +1,48 @@ +module ActionDispatch + module Http + module UploadedFile + def self.extended(object) + object.class_eval do + attr_accessor :original_path, :content_type + alias_method :local_path, :path if method_defined?(:path) + end + end + + # Take the basename of the upload's original filename. + # This handles the full Windows paths given by Internet Explorer + # (and perhaps other broken user agents) without affecting + # those which give the lone filename. + # The Windows regexp is adapted from Perl's File::Basename. + def original_filename + unless defined? @original_filename + @original_filename = + unless original_path.blank? + if original_path =~ /^(?:.*[:\\\/])?(.*)/m + $1 + else + File.basename original_path + end + end + end + @original_filename + end + end + + module Upload + # Convert nested Hashs to HashWithIndifferentAccess and replace + # file upload hashs with UploadedFile objects + def normalize_parameters(value) + if Hash === value && value.has_key?(:tempfile) + upload = value[:tempfile] + upload.extend(UploadedFile) + upload.original_path = value[:filename] + upload.content_type = value[:type] + upload + else + super + end + end + private :normalize_parameters + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb new file mode 100644 index 0000000000..40ceb5a9b6 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -0,0 +1,129 @@ +module ActionDispatch + module Http + module URL + # Returns the complete URL used for this request. + def url + protocol + host_with_port + request_uri + end + + # Returns 'https://' if this is an SSL request and 'http://' otherwise. + def protocol + ssl? ? 'https://' : 'http://' + end + + # Is this an SSL request? + def ssl? + @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' + end + + # Returns the \host for this request, such as "example.com". + def raw_host_with_port + if forwarded = env["HTTP_X_FORWARDED_HOST"] + forwarded.split(/,\s?/).last + else + env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}" + end + end + + # Returns the host for this request, such as example.com. + def host + raw_host_with_port.sub(/:\d+$/, '') + end + + # Returns a \host:\port string for this request, such as "example.com" or + # "example.com:8080". + def host_with_port + "#{host}#{port_string}" + end + + # Returns the port number of this request as an integer. + def port + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end + end + + # Returns the standard \port number for this request's protocol. + def standard_port + case protocol + when 'https://' then 443 + else 80 + end + end + + # Returns a \port suffix like ":8080" if the \port number of this request + # is not the default HTTP \port 80 or HTTPS \port 443. + def port_string + port == standard_port ? '' : ":#{port}" + end + + def server_port + @env['SERVER_PORT'].to_i + end + + # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify + # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". + def domain(tld_length = 1) + return nil unless named_host?(host) + + host.split('.').last(1 + tld_length).join('.') + end + + # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be + # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, + # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> + # in "www.rubyonrails.co.uk". + def subdomains(tld_length = 1) + return [] unless named_host?(host) + parts = host.split('.') + parts[0..-(tld_length+2)] + end + + # Returns the query string, accounting for server idiosyncrasies. + def query_string + @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].to_s.split('?', 2)[1] || '') + end + + # Returns the request URI, accounting for server idiosyncrasies. + # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. + def request_uri + if uri = @env['REQUEST_URI'] + # Remove domain, which webrick puts into the request_uri. + (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri + else + # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO. + uri = @env['PATH_INFO'].to_s + + if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) + uri = uri.sub(/#{script_filename}\//, '') + end + + env_qs = @env['QUERY_STRING'].to_s + uri += "?#{env_qs}" unless env_qs.empty? + + if uri.blank? + @env.delete('REQUEST_URI') + else + @env['REQUEST_URI'] = uri + end + end + end + + # Returns the interpreted \path to requested resource after all the installation + # directory of this application was taken into account. + def path + path = request_uri.to_s[/\A[^\?]*/] + path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') + path + end + + private + + def named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 5ec406e134..d07841218a 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -45,8 +45,6 @@ module ActionDispatch run_callbacks(:prepare) if @prepare_each_request @app.call(env) end - ensure - ActiveSupport::Notifications.instrument "action_dispatch.callback" end end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb new file mode 100644 index 0000000000..0dc03a1a7e --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -0,0 +1,216 @@ +module ActionDispatch + class Request + def cookie_jar + env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + end + end + + # Cookies are read and written through ActionController#cookies. + # + # The cookies being read are the ones received along with the request, the cookies + # being written will be sent out with the response. Reading a cookie does not get + # the cookie object itself back, just the value it holds. + # + # Examples for writing: + # + # # Sets a simple session cookie. + # cookies[:user_name] = "david" + # + # # Sets a cookie that expires in 1 hour. + # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } + # + # Examples for reading: + # + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # + # Example for deleting: + # + # cookies.delete :user_name + # + # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie: + # + # cookies[:key] = { + # :value => 'a yummy cookie', + # :expires => 1.year.from_now, + # :domain => 'domain.com' + # } + # + # cookies.delete(:key, :domain => 'domain.com') + # + # The option symbols for setting cookies are: + # + # * <tt>:value</tt> - The cookie's value or list of values (as an array). + # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root + # of the application. + # * <tt>:domain</tt> - The domain for which this cookie applies. + # * <tt>:expires</tt> - The time at which this cookie expires, as a Time object. + # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers. + # Default is +false+. + # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or + # only HTTP. Defaults to +false+. + class Cookies + class CookieJar < Hash #:nodoc: + def self.build(request) + new.tap do |hash| + hash.update(request.cookies) + end + end + + def initialize + @set_cookies = {} + @delete_cookies = {} + + super + end + + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. + def [](name) + 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 []=(key, options) + if options.is_a?(Hash) + options.symbolize_keys! + value = options[:value] + else + value = options + options = { :value => value } + end + + value = super(key.to_s, value) + + options[:path] ||= "/" + @set_cookies[key] = options + value + 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(key, options = {}) + options.symbolize_keys! + options[:path] ||= "/" + value = super(key.to_s) + @delete_cookies[key] = options + value + end + + # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: + # + # cookies.permanent[:prefers_open_id] = true + # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + # + # This jar is only meant for writing. You'll read permanent cookies through the regular accessor. + # + # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: + # + # cookies.permanent.signed[:remember_me] = current_user.id + # # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + def permanent + @permanent ||= PermanentCookieJar.new(self) + end + + # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from + # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed + # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will + # be raised. + # + # This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret. + # + # Example: + # + # cookies.signed[:discount] = 45 + # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/ + # + # cookies.signed[:discount] # => 45 + def signed + @signed ||= SignedCookieJar.new(self) + end + + def write(response) + @set_cookies.each { |k, v| response.set_cookie(k, v) } + @delete_cookies.each { |k, v| response.delete_cookie(k, v) } + end + end + + class PermanentCookieJar < CookieJar #:nodoc: + def initialize(parent_jar) + @parent_jar = parent_jar + end + + def []=(key, options) + if options.is_a?(Hash) + options.symbolize_keys! + else + options = { :value => options } + end + + options[:expires] = 20.years.from_now + @parent_jar[key] = options + end + + def signed + @signed ||= SignedCookieJar.new(self) + end + + def controller + @parent_jar.controller + end + + def method_missing(method, *arguments, &block) + @parent_jar.send(method, *arguments, &block) + end + end + + class SignedCookieJar < CookieJar #:nodoc: + def initialize(parent_jar) + unless ActionController::Base.cookie_verifier_secret + raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies" + end + + @parent_jar = parent_jar + @verifier = ActiveSupport::MessageVerifier.new(ActionController::Base.cookie_verifier_secret) + end + + def [](name) + if value = @parent_jar[name] + @verifier.verify(value) + end + end + + def []=(key, options) + if options.is_a?(Hash) + options.symbolize_keys! + options[:value] = @verifier.generate(options[:value]) + else + options = { :value => @verifier.generate(options) } + end + + @parent_jar[key] = options + end + + def method_missing(method, *arguments, &block) + @parent_jar.send(method, *arguments, &block) + end + end + + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + + if cookie_jar = env['action_dispatch.cookies'] + response = Rack::Response.new(body, status, headers) + cookie_jar.write(response) + response.to_a + else + [status, headers, body] + end + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/notifications.rb b/actionpack/lib/action_dispatch/middleware/notifications.rb new file mode 100644 index 0000000000..01d2cbb435 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/notifications.rb @@ -0,0 +1,24 @@ +module ActionDispatch + # Provide notifications in the middleware stack. Notice that for the before_dispatch + # and after_dispatch notifications, we just send the original env, so we don't pile + # up large env hashes in the queue. However, in exception cases, the whole env hash + # is actually useful, so we send it all. + class Notifications + def initialize(app) + @app = app + end + + def call(stack_env) + env = stack_env.dup + ActiveSupport::Notifications.instrument("action_dispatch.before_dispatch", :env => env) + + ActiveSupport::Notifications.instrument!("action_dispatch.after_dispatch", :env => env) do + @app.call(stack_env) + end + rescue Exception => exception + ActiveSupport::Notifications.instrument('action_dispatch.exception', + :env => stack_env, :exception => exception) + raise exception + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 10f04dcdf6..3bcd004e12 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -20,7 +20,7 @@ module ActionDispatch # * :exception - The exception raised; # class ShowExceptions - LOCALHOST = '127.0.0.1'.freeze + LOCALHOST = ['127.0.0.1', '::1'].freeze RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') @@ -61,11 +61,8 @@ module ActionDispatch def call(env) @app.call(env) rescue Exception => exception - ActiveSupport::Notifications.instrument 'action_dispatch.show_exception', - :env => env, :exception => exception do - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) - end + raise exception if env['action_dispatch.show_exceptions'] == false + render_exception(env, exception) end private @@ -88,7 +85,10 @@ module ActionDispatch def rescue_action_locally(request, exception) template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], :request => request, - :exception => exception + :exception => exception, + :application_trace => application_trace(exception), + :framework_trace => framework_trace(exception), + :full_trace => full_trace(exception) ) file = "rescues/#{@@rescue_templates[exception.class.name]}.erb" body = template.render(:file => file, :layout => 'rescues/layout.erb') @@ -118,7 +118,7 @@ module ActionDispatch # True if the request came from localhost, 127.0.0.1. def local_request?(request) - request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST + LOCALHOST.any?{ |local_ip| request.remote_addr == local_ip && request.remote_ip == local_ip } end def status_code(exception) @@ -148,9 +148,21 @@ module ActionDispatch end end - def clean_backtrace(exception) + def application_trace(exception) + clean_backtrace(exception, :silent) + end + + def framework_trace(exception) + clean_backtrace(exception, :noise) + end + + def full_trace(exception) + clean_backtrace(exception, :all) + end + + def clean_backtrace(exception, *args) defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? - Rails.backtrace_cleaner.clean(exception.backtrace) : + Rails.backtrace_cleaner.clean(exception.backtrace, *args) : exception.backtrace end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index 5224403dab..839df50999 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -11,13 +11,20 @@ clean_params.delete("controller") request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") + + def debug_hash(hash) + hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect}" }.join("\n") + end %> <h2 style="margin-top: 30px">Request</h2> <p><b>Parameters</b>: <pre><%=h request_dump %></pre></p> <p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p> -<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div> +<div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div> + +<p><a href="#" onclick="document.getElementById('env_dump').style.display='block'; return false;">Show env dump</a></p> +<div id="env_dump" style="display:none"><pre><%= debug_hash @request.env %></pre></div> <h2 style="margin-top: 30px">Response</h2> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb index 07b4919934..d18b162a93 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb @@ -1,8 +1,8 @@ <% traces = [ - ["Application Trace", @exception.application_backtrace], - ["Framework Trace", @exception.framework_backtrace], - ["Full Trace", @exception.clean_backtrace] + ["Application Trace", @application_trace], + ["Framework Trace", @framework_trace], + ["Full Trace", @full_trace] ] names = traces.collect {|name, trace| name} %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb index 693e56270a..bd6ffbab5d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb @@ -4,7 +4,7 @@ in <%=h @request.parameters['controller'].humanize %>Controller<% if @request.parameters['action'] %>#<%=h @request.parameters['action'] %><% end %> <% end %> </h1> -<pre><%=h @exception.clean_message %></pre> +<pre><%=h @exception.message %></pre> <%= render :file => "rescues/_trace.erb" %> <%= render :file => "rescues/_request_and_response.erb" %> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb new file mode 100644 index 0000000000..18978bfb39 --- /dev/null +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -0,0 +1,29 @@ +require "action_dispatch" +require "rails" + +module ActionDispatch + class Railtie < Rails::Railtie + plugin_name :action_dispatch + + require "action_dispatch/railties/subscriber" + subscriber ActionDispatch::Railties::Subscriber.new + + # Prepare dispatcher callbacks and run 'prepare' callbacks + initializer "action_dispatch.prepare_dispatcher" do |app| + # TODO: This used to say unless defined?(Dispatcher). Find out why and fix. + require 'rails/dispatcher' + + unless app.config.cache_classes + # Setup dev mode route reloading + routes_last_modified = app.routes_changed_at + reload_routes = lambda do + unless app.routes_changed_at == routes_last_modified + routes_last_modified = app.routes_changed_at + app.reload_routes! + end + end + ActionDispatch::Callbacks.before { |callbacks| reload_routes.call } + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/railties/subscriber.rb b/actionpack/lib/action_dispatch/railties/subscriber.rb new file mode 100644 index 0000000000..c08a844c6a --- /dev/null +++ b/actionpack/lib/action_dispatch/railties/subscriber.rb @@ -0,0 +1,17 @@ +module ActionDispatch + module Railties + class Subscriber < Rails::Subscriber + def before_dispatch(event) + request = Request.new(event.payload[:env]) + path = request.request_uri.inspect rescue "unknown" + + info "\n\nProcessing #{path} to #{request.formats.join(', ')} " << + "(for #{request.remote_ip} at #{event.time.to_s(:db)}) [#{request.method.to_s.upcase}]" + end + + def logger + ActionController::Base.logger + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index e99f979197..b598d6f7e2 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -193,9 +193,10 @@ module ActionDispatch # # With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>. # - # * <tt>:method</tt> - Allows you to specify which method can access the route. Possible values are <tt>:post</tt>, - # <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>, - # <tt>:any</tt> means that any method can access the route. + # * <tt>:method</tt> - Allows you to specify which HTTP method(s) can access the route. Possible values are + # <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. Use an array to specify more + # than one method, e.g. <tt>[ :get, :post ]</tt>. The default value is <tt>:any</tt>, <tt>:any</tt> means that any + # method can access the route. # # Example: # diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ce5c56ae1c..811c287355 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -380,7 +380,7 @@ module ActionDispatch end def controller - plural + options[:controller] || plural end def member_name @@ -418,28 +418,12 @@ module ActionDispatch def resource(*resources, &block) options = resources.extract_options! - if resources.length > 1 - raise ArgumentError if block_given? - resources.each { |r| resource(r, options) } - return self - end - - if path_names = options.delete(:path_names) - scope(:resources_path_names => path_names) do - resource(resources, options) - end + if verify_common_behavior_for(:resource, resources, options, &block) return self end resource = SingletonResource.new(resources.pop, options) - if @scope[:scope_level] == :resources - nested do - resource(resource.name, options, &block) - end - return self - end - scope(:path => resource.name.to_s, :controller => resource.controller) do with_scope_level(:resource, resource) do yield if block_given? @@ -459,28 +443,12 @@ module ActionDispatch def resources(*resources, &block) options = resources.extract_options! - if resources.length > 1 - raise ArgumentError if block_given? - resources.each { |r| resources(r, options) } - return self - end - - if path_names = options.delete(:path_names) - scope(:resources_path_names => path_names) do - resources(resources, options) - end + if verify_common_behavior_for(:resources, resources, options, &block) return self end resource = Resource.new(resources.pop, options) - if @scope[:scope_level] == :resources - nested do - resources(resource.name, options, &block) - end - return self - end - scope(:path => resource.name.to_s, :controller => resource.controller) do with_scope_level(:resources, resource) do yield if block_given? @@ -595,6 +563,29 @@ module ActionDispatch path_names[name.to_sym] || name.to_s end + def verify_common_behavior_for(method, resources, options, &block) + if resources.length > 1 + resources.each { |r| send(method, r, options, &block) } + return true + end + + if path_names = options.delete(:path_names) + scope(:resources_path_names => path_names) do + send(method, resources.pop, options, &block) + end + return true + end + + if @scope[:scope_level] == :resources + nested do + send(method, resources.pop, options, &block) + end + return true + end + + false + end + def with_exclusive_name_prefix(prefix) begin old_name_prefix = @scope[:name_prefix] diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index fc477afb17..0c33539b4a 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -12,29 +12,29 @@ module ActionDispatch # and a :method containing the required HTTP verb. # # # assert that POSTing to /items will call the create action on ItemsController - # assert_recognizes {:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post} + # assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}) # # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the # extras argument, appending the query string on the path directly will not work. For example: # # # assert that a path of '/items/list/1?view=print' returns the correct options - # assert_recognizes {:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" } + # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }) # # The +message+ parameter allows you to pass in an error message that is displayed upon failure. # # ==== Examples # # Check the default route (i.e., the index action) - # assert_recognizes {:controller => 'items', :action => 'index'}, 'items' + # assert_recognizes({:controller => 'items', :action => 'index'}, 'items') # # # Test a specific action - # assert_recognizes {:controller => 'items', :action => 'list'}, 'items/list' + # assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list') # # # Test an action with a parameter - # assert_recognizes {:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1' + # assert_recognizes({:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1') # # # Test a custom route - # assert_recognizes {:controller => 'items', :action => 'show', :id => '1'}, 'view/item1' + # assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1') # # # Check a Simply RESTful generated route # assert_recognizes list_items_url, 'items/list' @@ -103,7 +103,7 @@ module ActionDispatch # assert_routing '/home', :controller => 'home', :action => 'index' # # # Test a route generated with a specific controller, action, and parameter (id) - # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', id => 23 + # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', :id => 23 # # # Assert a basic route (controller + default action), with an error message if it fails # assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly' @@ -112,7 +112,7 @@ module ActionDispatch # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"} # # # Tests a route with a HTTP method - # assert_routing { :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" } + # assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" }) def assert_routing(path, options, defaults={}, extras={}, message=nil) assert_recognizes(options, path, extras, message) diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index 90863fca08..885945fde3 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -27,10 +27,10 @@ module ActionView def debug(object) begin Marshal::dump(object) - "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>" + "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>".html_safe! rescue Exception => e # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback - "<code class='debug_dump'>#{h(object.inspect)}</code>" + "<code class='debug_dump'>#{h(object.inspect)}</code>".html_safe! end end end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 7688e786b1..048bedc7ba 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -55,6 +55,9 @@ module ActionView # * Any other key creates standard HTML attributes for the tag. # # ==== Examples + # select_tag "people", options_from_collection_for_select(@people, "name", "id") + # # <select id="people" name="people"><option value="1">David</option></select> + # # select_tag "people", "<option>David</option>" # # => <select id="people" name="people"><option>David</option></select> # diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index f03ffe5ef4..657d26f0a2 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -22,7 +22,7 @@ module ActionView # # Custom Use (only the mentioned tags and attributes are allowed, nothing else) # - # <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style) + # <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style) %> # # Add table tags to the default allowed tags # diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index be15e227b9..814d86812d 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -226,8 +226,7 @@ module ActionView # Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags. # # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile]. - # <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/] - # is available</i>. + # <i>This method is only available if RedCloth[http://redcloth.org/] is available</i>. # # ==== Examples # textilize("*This is Textile!* Rejoice!") @@ -263,8 +262,7 @@ module ActionView # but without the bounding <p> tag that RedCloth adds. # # You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile]. - # <i>This method is requires RedCloth[http://whytheluckystiff.net/ruby/redcloth/] - # to be available</i>. + # <i>This method is only available if RedCloth[http://redcloth.org/] is available</i>. # # ==== Examples # textilize_without_paragraph("*This is Textile!* Rejoice!") diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 10913c0fdb..7b04638ccc 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -92,6 +92,7 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase middleware.use "ActionDispatch::ShowExceptions" middleware.use "ActionDispatch::Callbacks" middleware.use "ActionDispatch::ParamsParser" + middleware.use "ActionDispatch::Cookies" middleware.use "ActionDispatch::Flash" middleware.use "ActionDispatch::Head" }.build(routes || ActionController::Routing::Routes) diff --git a/actionpack/test/activerecord/controller_runtime_test.rb b/actionpack/test/activerecord/controller_runtime_test.rb index 37c7738301..d6f7cd80ab 100644 --- a/actionpack/test/activerecord/controller_runtime_test.rb +++ b/actionpack/test/activerecord/controller_runtime_test.rb @@ -37,8 +37,8 @@ module ControllerRuntimeSubscriberTest get :show wait - assert_equal 2, @logger.logged(:info).size - assert_match /\(Views: [\d\.]+ms | ActiveRecord: [\d\.]+ms\)/, @logger.logged(:info)[1] + assert_equal 1, @logger.logged(:info).size + assert_match /\(Views: [\d\.]+ms | ActiveRecord: [\d\.]+ms\)/, @logger.logged(:info)[0] end class SyncSubscriberTest < ActionController::TestCase diff --git a/actionpack/test/controller/cookie_test.rb b/actionpack/test/controller/cookie_test.rb index c8e8b3857e..f5ccef8aaf 100644 --- a/actionpack/test/controller/cookie_test.rb +++ b/actionpack/test/controller/cookie_test.rb @@ -54,12 +54,12 @@ class CookieTest < ActionController::TestCase cookies.permanent[:user_name] = "Jamie" head :ok end - + def set_signed_cookie cookies.signed[:user_id] = 45 head :ok end - + def set_permanent_signed_cookie cookies.permanent.signed[:remember_me] = 100 head :ok @@ -120,28 +120,6 @@ class CookieTest < ActionController::TestCase assert_equal({"user_name" => nil}, @response.cookies) end - def test_cookiejar_accessor - @request.cookies["user_name"] = "david" - @controller.request = @request - jar = ActionController::CookieJar.build(@controller.request, @controller.response) - assert_equal "david", jar["user_name"] - assert_equal nil, jar["something_else"] - end - - def test_cookiejar_accessor_with_array_value - @request.cookies["pages"] = %w{1 2 3} - @controller.request = @request - jar = ActionController::CookieJar.build(@controller.request, @controller.response) - assert_equal %w{1 2 3}, jar["pages"] - end - - def test_cookiejar_delete_removes_item_and_returns_its_value - @request.cookies["user_name"] = "david" - @controller.response = @response - jar = ActionController::CookieJar.build(@controller.request, @controller.response) - assert_equal "david", jar.delete("user_name") - end - def test_delete_cookie_with_path get :delete_cookie_with_path assert_cookie_header "user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT" @@ -157,19 +135,24 @@ class CookieTest < ActionController::TestCase assert_match /Jamie/, @response.headers["Set-Cookie"] assert_match %r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"] end - + def test_signed_cookie get :set_signed_cookie assert_equal 45, @controller.send(:cookies).signed[:user_id] end - + + def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature + get :set_signed_cookie + assert_nil @controller.send(:cookies).signed[:non_existant_attribute] + end + def test_permanent_signed_cookie get :set_permanent_signed_cookie assert_match %r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"] assert_equal 100, @controller.send(:cookies).signed[:remember_me] end - + private def assert_cookie_header(expected) header = @response.headers["Set-Cookie"] diff --git a/actionpack/test/controller/filter_params_test.rb b/actionpack/test/controller/filter_params_test.rb index d0635669c2..45949636c3 100644 --- a/actionpack/test/controller/filter_params_test.rb +++ b/actionpack/test/controller/filter_params_test.rb @@ -9,23 +9,6 @@ end class FilterParamTest < ActionController::TestCase tests FilterParamController - class MockLogger - attr_reader :logged - attr_accessor :level - - def initialize - @level = Logger::DEBUG - end - - def method_missing(method, *args) - @logged ||= [] - @logged << args.first unless block_given? - @logged << yield if block_given? - end - end - - setup :set_logger - def test_filter_parameters_must_have_one_word assert_raises RuntimeError do FilterParamController.filter_parameter_logging @@ -65,14 +48,4 @@ class FilterParamTest < ActionController::TestCase assert !FilterParamController.action_methods.include?('filter_parameters') assert_raise(NoMethodError) { @controller.filter_parameters([{'password' => '[FILTERED]'}]) } end - - private - - def set_logger - @controller.logger = MockLogger.new - end - - def logs - @logs ||= @controller.logger.logged.compact.map {|l| l.to_s.strip} - end end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 85a2e7f44b..3c651ebebc 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -220,7 +220,7 @@ class FlashIntegrationTest < ActionController::IntegrationTest def with_test_route_set with_routing do |set| set.draw do |map| - match ':action', :to => ActionDispatch::Session::CookieStore.new(TestController, :key => SessionKey, :secret => SessionSecret) + match ':action', :to => ActionDispatch::Session::CookieStore.new(FlashIntegrationTest::TestController, :key => FlashIntegrationTest::SessionKey, :secret => FlashIntegrationTest::SessionSecret) end yield end diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb index 1f9bf7f0fb..964780eaf2 100644 --- a/actionpack/test/controller/new_base/base_test.rb +++ b/actionpack/test/controller/new_base/base_test.rb @@ -3,6 +3,8 @@ require 'abstract_unit' # Tests the controller dispatching happy path module Dispatching class SimpleController < ActionController::Base + before_filter :authenticate + def index render :text => "success" end @@ -12,12 +14,20 @@ module Dispatching end def modify_response_body_twice - ret = (self.response_body = "success") + ret = (self.response_body = "success") self.response_body = "#{ret}!" end def modify_response_headers end + + def show_actions + render :text => "actions: #{action_methods.to_a.join(', ')}" + end + + protected + def authenticate + end end class EmptyController < ActionController::Base ; end @@ -64,5 +74,21 @@ module Dispatching assert_equal 'empty', EmptyController.controller_name assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name end + + test "action methods" do + assert_equal Set.new(%w( + modify_response_headers + modify_response_body_twice + index + modify_response_body + show_actions + )), SimpleController.action_methods + + assert_equal Set.new, EmptyController.action_methods + assert_equal Set.new, Submodule::ContainedEmptyController.action_methods + + get "/dispatching/simple/show_actions" + assert_body "actions: modify_response_headers, modify_response_body_twice, index, modify_response_body, show_actions" + end end end diff --git a/actionpack/test/controller/subscriber_test.rb b/actionpack/test/controller/subscriber_test.rb index ef1a325799..24132ee928 100644 --- a/actionpack/test/controller/subscriber_test.rb +++ b/actionpack/test/controller/subscriber_test.rb @@ -66,15 +66,10 @@ module ActionControllerSubscriberTest def test_process_action get :show wait - assert_equal 2, logs.size - assert_match /Processed\sAnother::SubscribersController#show/, logs[0] - end - - def test_process_action_formats - get :show - wait - assert_equal 2, logs.size - assert_match /text\/html/, logs[0] + assert_equal 1, logs.size + assert_match /Completed/, logs.first + assert_match /\[200\]/, logs.first + assert_match /Another::SubscribersController#show/, logs.first end def test_process_action_without_parameters @@ -87,23 +82,14 @@ module ActionControllerSubscriberTest get :show, :id => '10' wait - assert_equal 3, logs.size - assert_equal 'Parameters: {"id"=>"10"}', logs[1] + assert_equal 2, logs.size + assert_equal 'Parameters: {"id"=>"10"}', logs[0] end def test_process_action_with_view_runtime get :show wait - assert_match /\(Views: [\d\.]+ms\)/, logs[1] - end - - def test_process_action_with_status_and_request_uri - get :show - wait - last = logs.last - assert_match /Completed/, last - assert_match /200/, last - assert_match /another\/subscribers\/show/, last + assert_match /\(Views: [\d\.]+ms\)/, logs[0] end def test_process_action_with_filter_parameters @@ -112,7 +98,7 @@ module ActionControllerSubscriberTest get :show, :lifo => 'Pratik', :amount => '420', :step => '1' wait - params = logs[1] + params = logs[0] assert_match /"amount"=>"\[FILTERED\]"/, params assert_match /"lifo"=>"\[FILTERED\]"/, params assert_match /"step"=>"1"/, params @@ -122,7 +108,7 @@ module ActionControllerSubscriberTest get :redirector wait - assert_equal 3, logs.size + assert_equal 2, logs.size assert_equal "Redirected to http://foo.bar/", logs[0] end @@ -130,7 +116,7 @@ module ActionControllerSubscriberTest get :data_sender wait - assert_equal 3, logs.size + assert_equal 2, logs.size assert_match /Sent data omg\.txt/, logs[0] end @@ -138,7 +124,7 @@ module ActionControllerSubscriberTest get :file_sender wait - assert_equal 3, logs.size + assert_equal 2, logs.size assert_match /Sent file/, logs[0] assert_match /test\/fixtures\/company\.rb/, logs[0] end @@ -147,7 +133,7 @@ module ActionControllerSubscriberTest get :xfile_sender wait - assert_equal 3, logs.size + assert_equal 2, logs.size assert_match /Sent X\-Sendfile header/, logs[0] assert_match /test\/fixtures\/company\.rb/, logs[0] end @@ -157,7 +143,7 @@ module ActionControllerSubscriberTest get :with_fragment_cache wait - assert_equal 4, logs.size + assert_equal 3, logs.size assert_match /Exist fragment\? views\/foo/, logs[0] assert_match /Write fragment views\/foo/, logs[1] ensure @@ -169,7 +155,7 @@ module ActionControllerSubscriberTest get :with_page_cache wait - assert_equal 3, logs.size + assert_equal 2, logs.size assert_match /Write page/, logs[0] assert_match /\/index\.html/, logs[0] ensure diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb index f3ea5209f4..9df882ce75 100644 --- a/actionpack/test/dispatch/callbacks_test.rb +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -85,18 +85,6 @@ class DispatcherTest < Test::Unit::TestCase assert_equal 4, Foo.b end - def test_should_send_an_instrumentation_callback_for_async_processing - ActiveSupport::Notifications.expects(:instrument).with("action_dispatch.callback") - dispatch - end - - def test_should_send_an_instrumentation_callback_for_async_processing_even_on_failure - ActiveSupport::Notifications.notifier.expects(:publish) - assert_raise RuntimeError do - dispatch { |env| raise "OMG" } - end - end - private def dispatch(cache_classes = true, &block) diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 02f63f7006..4697fa3e2b 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -13,8 +13,7 @@ class ResponseTest < ActiveSupport::TestCase assert_equal({ "Content-Type" => "text/html; charset=utf-8", "Cache-Control" => "max-age=0, private, must-revalidate", - "ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"', - "Set-Cookie" => "" + "ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"' }, headers) parts = [] @@ -30,8 +29,7 @@ class ResponseTest < ActiveSupport::TestCase assert_equal({ "Content-Type" => "text/html; charset=utf-8", "Cache-Control" => "max-age=0, private, must-revalidate", - "ETag" => '"ebb5e89e8a94e9dd22abf5d915d112b2"', - "Set-Cookie" => "" + "ETag" => '"ebb5e89e8a94e9dd22abf5d915d112b2"' }, headers) end @@ -44,8 +42,7 @@ class ResponseTest < ActiveSupport::TestCase assert_equal 200, status assert_equal({ "Content-Type" => "text/html; charset=utf-8", - "Cache-Control" => "no-cache", - "Set-Cookie" => "" + "Cache-Control" => "no-cache" }, headers) parts = [] diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 5845c791a0..6dccabdb3f 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -65,7 +65,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resources :companies do resources :people - resource :avatar + resource :avatar, :controller => :avatar end resources :images do @@ -294,34 +294,34 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest def test_projects with_test_routes do get '/projects' - assert_equal 'projects#index', @response.body + assert_equal 'project#index', @response.body assert_equal '/projects', projects_path post '/projects' - assert_equal 'projects#create', @response.body + assert_equal 'project#create', @response.body get '/projects.xml' - assert_equal 'projects#index', @response.body + assert_equal 'project#index', @response.body assert_equal '/projects.xml', projects_path(:format => 'xml') get '/projects/new' - assert_equal 'projects#new', @response.body + assert_equal 'project#new', @response.body assert_equal '/projects/new', new_project_path get '/projects/new.xml' - assert_equal 'projects#new', @response.body + assert_equal 'project#new', @response.body assert_equal '/projects/new.xml', new_project_path(:format => 'xml') get '/projects/1' - assert_equal 'projects#show', @response.body + assert_equal 'project#show', @response.body assert_equal '/projects/1', project_path(:id => '1') get '/projects/1.xml' - assert_equal 'projects#show', @response.body + assert_equal 'project#show', @response.body assert_equal '/projects/1.xml', project_path(:id => '1', :format => 'xml') get '/projects/1/edit' - assert_equal 'projects#edit', @response.body + assert_equal 'project#edit', @response.body assert_equal '/projects/1/edit', edit_project_path(:id => '1') end end @@ -383,7 +383,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/projects/1/companies/1/people', project_company_people_path(:project_id => '1', :company_id => '1') get '/projects/1/companies/1/avatar' - assert_equal 'avatars#show', @response.body + assert_equal 'avatar#show', @response.body assert_equal '/projects/1/companies/1/avatar', project_company_avatar_path(:project_id => '1', :company_id => '1') end end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index ab7b9bc31b..d2c1758af1 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -137,7 +137,7 @@ class CookieStoreTest < ActionController::IntegrationTest with_test_route_set do get '/no_session_access' assert_response :success - assert_equal "", headers['Set-Cookie'] + assert_equal nil, headers['Set-Cookie'] end end @@ -147,7 +147,7 @@ class CookieStoreTest < ActionController::IntegrationTest "fef868465920f415f2c0652d6910d3af288a0367" get '/no_session_access' assert_response :success - assert_equal "", headers['Set-Cookie'] + assert_equal nil, headers['Set-Cookie'] end end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 951fb4a22e..def86c8323 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -53,19 +53,21 @@ class ShowExceptionsTest < ActionController::IntegrationTest test "rescue locally from a local request" do @app = ProductionApp - self.remote_addr = '127.0.0.1' + ['127.0.0.1', '::1'].each do |ip_address| + self.remote_addr = ip_address - get "/" - assert_response 500 - assert_match /puke/, body + get "/" + assert_response 500 + assert_match /puke/, body - get "/not_found" - assert_response 404 - assert_match /#{ActionController::UnknownAction.name}/, body + get "/not_found" + assert_response 404 + assert_match /#{ActionController::UnknownAction.name}/, body - get "/method_not_allowed" - assert_response 405 - assert_match /ActionController::MethodNotAllowed/, body + get "/method_not_allowed" + assert_response 405 + assert_match /ActionController::MethodNotAllowed/, body + end end test "localize public rescue message" do @@ -104,27 +106,4 @@ class ShowExceptionsTest < ActionController::IntegrationTest assert_response 405 assert_match /ActionController::MethodNotAllowed/, body end - - test "publishes notifications" do - # Wait pending notifications to be published - ActiveSupport::Notifications.notifier.wait - - @app, event = ProductionApp, nil - self.remote_addr = '127.0.0.1' - - ActiveSupport::Notifications.subscribe('action_dispatch.show_exception') do |*args| - event = args - end - - get "/" - assert_response 500 - assert_match /puke/, body - - ActiveSupport::Notifications.notifier.wait - - assert_equal 'action_dispatch.show_exception', event.first - assert_kind_of Hash, event.last[:env] - assert_equal 'GET', event.last[:env]["REQUEST_METHOD"] - assert_kind_of RuntimeError, event.last[:exception] - end end diff --git a/actionpack/test/dispatch/subscriber_test.rb b/actionpack/test/dispatch/subscriber_test.rb new file mode 100644 index 0000000000..a7f1a2659a --- /dev/null +++ b/actionpack/test/dispatch/subscriber_test.rb @@ -0,0 +1,112 @@ +require "abstract_unit" +require "rails/subscriber/test_helper" +require "action_dispatch/railties/subscriber" + +module DispatcherSubscriberTest + Boomer = lambda do |env| + req = ActionDispatch::Request.new(env) + case req.path + when "/" + [200, {}, []] + else + raise "puke!" + end + end + + App = ActionDispatch::Notifications.new(Boomer) + + def setup + Rails::Subscriber.add(:action_dispatch, ActionDispatch::Railties::Subscriber.new) + @app = App + super + + @events = [] + ActiveSupport::Notifications.subscribe do |*args| + @events << args + end + end + + def set_logger(logger) + ActionController::Base.logger = logger + end + + def test_publishes_notifications + get "/" + wait + + assert_equal 2, @events.size + before, after = @events + + assert_equal 'action_dispatch.before_dispatch', before[0] + assert_kind_of Hash, before[4][:env] + assert_equal 'GET', before[4][:env]["REQUEST_METHOD"] + + assert_equal 'action_dispatch.after_dispatch', after[0] + assert_kind_of Hash, after[4][:env] + assert_equal 'GET', after[4][:env]["REQUEST_METHOD"] + end + + def test_publishes_notifications_even_on_failures + begin + get "/puke" + rescue + end + + wait + + assert_equal 3, @events.size + before, after, exception = @events + + assert_equal 'action_dispatch.before_dispatch', before[0] + assert_kind_of Hash, before[4][:env] + assert_equal 'GET', before[4][:env]["REQUEST_METHOD"] + + assert_equal 'action_dispatch.after_dispatch', after[0] + assert_kind_of Hash, after[4][:env] + assert_equal 'GET', after[4][:env]["REQUEST_METHOD"] + + assert_equal 'action_dispatch.exception', exception[0] + assert_kind_of Hash, exception[4][:env] + assert_equal 'GET', exception[4][:env]["REQUEST_METHOD"] + assert_kind_of RuntimeError, exception[4][:exception] + end + + def test_subscriber_logs_notifications + get "/" + wait + + log = @logger.logged(:info).first + assert_equal 1, @logger.logged(:info).size + + assert_match %r{^Processing "/" to text/html}, log + assert_match %r{\(for 127\.0\.0\.1}, log + assert_match %r{\[GET\]}, log + end + + def test_subscriber_has_its_logged_flushed_after_request + assert_equal 0, @logger.flush_count + get "/" + wait + assert_equal 1, @logger.flush_count + end + + def test_subscriber_has_its_logged_flushed_even_after_busted_requests + assert_equal 0, @logger.flush_count + begin + get "/puke" + rescue + end + wait + assert_equal 1, @logger.flush_count + end + + class SyncSubscriberTest < ActionController::IntegrationTest + include Rails::Subscriber::SyncTestHelper + include DispatcherSubscriberTest + end + + class AsyncSubscriberTest < ActionController::IntegrationTest + include Rails::Subscriber::AsyncTestHelper + include DispatcherSubscriberTest + end +end
\ No newline at end of file diff --git a/actionpack/test/fixtures/reply.rb b/actionpack/test/fixtures/reply.rb index 04598437c2..19cba93673 100644 --- a/actionpack/test/fixtures/reply.rb +++ b/actionpack/test/fixtures/reply.rb @@ -1,5 +1,5 @@ class Reply < ActiveRecord::Base - named_scope :base + scope :base belongs_to :topic, :include => [:replies] belongs_to :developer |