diff options
author | Joshua Peek <josh@joshpeek.com> | 2009-05-29 16:06:21 -0500 |
---|---|---|
committer | Joshua Peek <josh@joshpeek.com> | 2009-05-29 16:06:21 -0500 |
commit | 69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3 (patch) | |
tree | 044c2131cc87d21ee54027511aae2b7f2d2ae26a /actionpack/lib/action_controller | |
parent | 5f3f100ce2d689480da85abc88e5e940cf90189e (diff) | |
parent | 5ec2c7dc29b36d85b2658465b8a979deb0529d7e (diff) | |
download | rails-69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3.tar.gz rails-69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3.tar.bz2 rails-69742ca8fa05509f7d7c5512cb6d8e002ecb3ab3.zip |
Merge branch 'master' into active_model
Conflicts:
activemodel/lib/active_model/core.rb
activemodel/test/cases/state_machine/event_test.rb
activemodel/test/cases/state_machine/state_transition_test.rb
activerecord/lib/active_record/validations.rb
activerecord/test/cases/validations/i18n_validation_test.rb
activeresource/lib/active_resource.rb
activeresource/test/abstract_unit.rb
Diffstat (limited to 'actionpack/lib/action_controller')
71 files changed, 2267 insertions, 2808 deletions
diff --git a/actionpack/lib/action_controller/abstract.rb b/actionpack/lib/action_controller/abstract.rb index 3f5c4a185f..f46b91627f 100644 --- a/actionpack/lib/action_controller/abstract.rb +++ b/actionpack/lib/action_controller/abstract.rb @@ -1,5 +1,9 @@ +require "active_support/core_ext/module/attr_internal" +require "active_support/core_ext/module/delegation" + module AbstractController autoload :Base, "action_controller/abstract/base" + autoload :Benchmarker, "action_controller/abstract/benchmarker" autoload :Callbacks, "action_controller/abstract/callbacks" autoload :Helpers, "action_controller/abstract/helpers" autoload :Layouts, "action_controller/abstract/layouts" @@ -7,4 +11,4 @@ module AbstractController autoload :Renderer, "action_controller/abstract/renderer" # === Exceptions autoload :ActionNotFound, "action_controller/abstract/exceptions" -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/base.rb b/actionpack/lib/action_controller/abstract/base.rb index ade7719cc0..87083a4d79 100644 --- a/actionpack/lib/action_controller/abstract/base.rb +++ b/actionpack/lib/action_controller/abstract/base.rb @@ -1,41 +1,115 @@ +require 'active_support/core_ext/module/attr_internal' + module AbstractController + class Error < StandardError; end + + class DoubleRenderError < Error + DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"." + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + class Base - attr_internal :response_body attr_internal :response_obj attr_internal :action_name - - def self.process(action) - new.process(action) - end - - def self.inherited(klass) + + class << self + attr_reader :abstract + + def abstract! + @abstract = true + end + + alias_method :abstract?, :abstract + + def inherited(klass) + ::AbstractController::Base.subclasses << klass.to_s + super + end + + def subclasses + @subclasses ||= [] + end + + def internal_methods + controller = self + controller = controller.superclass until controller.abstract? + controller.public_instance_methods(true) + end + + def process(action) + new.process(action.to_s) + end + + def hidden_actions + [] + end + + def action_methods + @action_methods ||= + # 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 + end end - + + abstract! + def initialize self.response_obj = {} end - - def process(action_name) - unless respond_to_action?(action_name) - raise ActionNotFound, "The action '#{action_name}' could not be found" + + def process(action) + @_action_name = action_name = action.to_s + + unless action_name = method_for_action(action_name) + raise ActionNotFound, "The action '#{action}' could not be found" end - - @_action_name = action_name - process_action - self.response_obj[:body] = self.response_body + + process_action(action_name) self end - + private - - def process_action - respond_to?(action_name) ? send(action_name) : send(:action_missing, action_name) + def action_methods + self.class.action_methods + end + + def action_method?(action) + action_methods.include?(action) end - - def respond_to_action?(action_name) - respond_to?(action_name) || respond_to?(:action_missing, true) + + # It is possible for respond_to?(action_name) to be false and + # respond_to?(:action_missing) to be false if respond_to_action? + # is overridden in a subclass. For instance, ActionController::Base + # overrides it to include the case where a template matching the + # action_name is found. + def process_action(method_name) + send_action(method_name) + end + + alias send_action send + + def _handle_action_missing + action_missing(@_action_name) + end + + # Override this to change the conditions that will raise an + # ActionNotFound error. If you accept a difference case, + # you must handle it by also overriding process_action and + # handling the case. + 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
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/benchmarker.rb b/actionpack/lib/action_controller/abstract/benchmarker.rb new file mode 100644 index 0000000000..6999329144 --- /dev/null +++ b/actionpack/lib/action_controller/abstract/benchmarker.rb @@ -0,0 +1,28 @@ +module AbstractController + module Benchmarker + extend ActiveSupport::Concern + + depends_on Logger + + module ClassMethods + def benchmark(title, log_level = ::Logger::DEBUG, use_silence = true) + if logger && logger.level >= log_level + result = nil + ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } + logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)") + result + else + yield + end + end + + # Silences the logger for the duration of the block. + def silence + old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger + yield + ensure + logger.level = old_logger_level if logger + end + end + end +end diff --git a/actionpack/lib/action_controller/abstract/callbacks.rb b/actionpack/lib/action_controller/abstract/callbacks.rb index c8b509081c..6c67315c58 100644 --- a/actionpack/lib/action_controller/abstract/callbacks.rb +++ b/actionpack/lib/action_controller/abstract/callbacks.rb @@ -1,28 +1,31 @@ module AbstractController module Callbacks - setup do - include ActiveSupport::NewCallbacks - define_callbacks :process_action + extend ActiveSupport::Concern + + depends_on ActiveSupport::NewCallbacks + + included do + define_callbacks :process_action, "response_body" end - - def process_action - _run_process_action_callbacks(action_name) do + + def process_action(method_name) + _run_process_action_callbacks(method_name) do super end end - + module ClassMethods def _normalize_callback_options(options) if only = options[:only] - only = Array(only).map {|o| "action_name == :#{o}"}.join(" || ") + only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") options[:per_key] = {:if => only} end if except = options[:except] - except = Array(except).map {|e| "action_name == :#{e}"}.join(" || ") + except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") options[:per_key] = {:unless => except} end end - + [:before, :after, :around].each do |filter| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{filter}_filter(*names, &blk) @@ -33,8 +36,28 @@ module AbstractController process_action_callback(:#{filter}, name, options) end end + + def prepend_#{filter}_filter(*names, &blk) + options = names.last.is_a?(Hash) ? names.pop : {} + _normalize_callback_options(options) + names.push(blk) if block_given? + names.each do |name| + process_action_callback(:#{filter}, name, options.merge(:prepend => true)) + end + end + + def skip_#{filter}_filter(*names, &blk) + options = names.last.is_a?(Hash) ? names.pop : {} + _normalize_callback_options(options) + names.push(blk) if block_given? + names.each do |name| + skip_process_action_callback(:#{filter}, name, options) + end + end + + alias_method :append_#{filter}_filter, :#{filter}_filter RUBY_EVAL end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/exceptions.rb b/actionpack/lib/action_controller/abstract/exceptions.rb index ec4680629b..2f6c55f068 100644 --- a/actionpack/lib/action_controller/abstract/exceptions.rb +++ b/actionpack/lib/action_controller/abstract/exceptions.rb @@ -1,3 +1,3 @@ module AbstractController - class ActionNotFound < StandardError ; end -end
\ No newline at end of file + class ActionNotFound < StandardError; end +end diff --git a/actionpack/lib/action_controller/abstract/helpers.rb b/actionpack/lib/action_controller/abstract/helpers.rb index 1f0b38417b..43832f1e2d 100644 --- a/actionpack/lib/action_controller/abstract/helpers.rb +++ b/actionpack/lib/action_controller/abstract/helpers.rb @@ -1,19 +1,14 @@ module AbstractController module Helpers + extend ActiveSupport::Concern + depends_on Renderer - - setup do + + included do extlib_inheritable_accessor :master_helper_module self.master_helper_module = Module.new end - - # def self.included(klass) - # klass.class_eval do - # extlib_inheritable_accessor :master_helper_module - # self.master_helper_module = Module.new - # end - # end - + def _action_view @_action_view ||= begin av = super @@ -21,19 +16,38 @@ module AbstractController av end end - + module ClassMethods def inherited(klass) klass.master_helper_module = Module.new klass.master_helper_module.__send__ :include, master_helper_module - + super end - + + # Makes all the (instance) methods in the helper module available to templates rendered through this controller. + # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules + # available to the templates. def add_template_helper(mod) master_helper_module.module_eval { include mod } end - + + # Declare a controller method as a helper. For example, the following + # makes the +current_user+ controller method available to the view: + # class ApplicationController < ActionController::Base + # helper_method :current_user, :logged_in? + # + # def current_user + # @current_user ||= User.find_by_id(session[:user]) + # end + # + # def logged_in? + # current_user != nil + # end + # end + # + # In a view: + # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> def helper_method(*meths) meths.flatten.each do |meth| master_helper_module.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 @@ -43,17 +57,16 @@ module AbstractController ruby_eval end end - - def helper(*args, &blk) + + def helper(*args, &block) args.flatten.each do |arg| case arg when Module add_template_helper(arg) end end - master_helper_module.module_eval(&blk) if block_given? + master_helper_module.module_eval(&block) if block_given? end end - end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/layouts.rb b/actionpack/lib/action_controller/abstract/layouts.rb index 478b301a26..b3f21f7b22 100644 --- a/actionpack/lib/action_controller/abstract/layouts.rb +++ b/actionpack/lib/action_controller/abstract/layouts.rb @@ -1,25 +1,34 @@ module AbstractController module Layouts - + extend ActiveSupport::Concern + depends_on Renderer - + + included do + extlib_inheritable_accessor :_layout_conditions + self._layout_conditions = {} + end + module ClassMethods - def layout(layout) + def layout(layout, conditions = {}) unless [String, Symbol, FalseClass, NilClass].include?(layout.class) raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" end - + + conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } + self._layout_conditions = conditions + @_layout = layout || false # Converts nil to false _write_layout_method end - + def _implied_layout_name name.underscore end - + # Takes the specified layout and creates a _layout method to be called # by _default_layout - # + # # If the specified layout is a: # String:: return the string # Symbol:: call the method specified by the symbol @@ -30,15 +39,15 @@ module AbstractController def _write_layout_method case @_layout when String - self.class_eval %{def _layout() #{@_layout.inspect} end} + self.class_eval %{def _layout(details) #{@_layout.inspect} end} when Symbol - self.class_eval %{def _layout() #{@_layout} end} + self.class_eval %{def _layout(details) #{@_layout} end} when false - self.class_eval %{def _layout() end} + self.class_eval %{def _layout(details) end} else self.class_eval %{ - def _layout - if view_paths.find_by_parts?("#{_implied_layout_name}", formats, "layouts") + def _layout(details) + if view_paths.find_by_parts?("#{_implied_layout_name}", details, "layouts") "#{_implied_layout_name}" else super @@ -48,35 +57,51 @@ module AbstractController end end end - - def _render_template(template, options) - _action_view._render_template_with_layout(template, options[:_layout]) - end - + private - - def _layout() end # This will be overwritten - - def _layout_for_name(name) + # This will be overwritten + def _layout(details) + end + + # :api: plugin + # ==== + # Override this to mutate the inbound layout name + def _layout_for_name(name, details = {:formats => formats}) unless [String, FalseClass, NilClass].include?(name.class) raise ArgumentError, "String, false, or nil expected; you passed #{name.inspect}" end - - name && view_paths.find_by_parts(name, formats, "layouts") + + name && view_paths.find_by_parts(name, details, _layout_prefix(name)) end - - def _default_layout(require_layout = false) - if require_layout && !_layout - raise ArgumentError, + + # TODO: Decide if this is the best hook point for the feature + def _layout_prefix(name) + "layouts" + end + + def _default_layout(require_layout = false, details = {:formats => formats}) + if require_layout && _action_has_layout? && !_layout(details) + raise ArgumentError, "There was no default layout for #{self.class} in #{view_paths.inspect}" end - + begin - layout = _layout_for_name(_layout) + _layout_for_name(_layout(details), details) if _action_has_layout? rescue NameError => e - raise NoMethodError, + raise NoMethodError, "You specified #{@_layout.inspect} as the layout, but no such method was found" end end + + def _action_has_layout? + conditions = _layout_conditions + if only = conditions[:only] + only.include?(action_name) + elsif except = conditions[:except] + !except.include?(action_name) + else + true + end + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/logger.rb b/actionpack/lib/action_controller/abstract/logger.rb index 4117369bd4..d6fa843485 100644 --- a/actionpack/lib/action_controller/abstract/logger.rb +++ b/actionpack/lib/action_controller/abstract/logger.rb @@ -1,7 +1,45 @@ +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/logger' + module AbstractController module Logger - setup do + extend ActiveSupport::Concern + + class DelayedLog + def initialize(&blk) + @blk = blk + end + + def to_s + @blk.call + end + alias to_str to_s + end + + included do cattr_accessor :logger end + + def process(action) + ret = super + + if logger + log = DelayedLog.new do + "\n\nProcessing #{self.class.name}\##{action_name} " \ + "to #{request.formats} " \ + "(for #{request_origin}) [#{request.method.to_s.upcase}]" + end + + logger.info(log) + end + + ret + end + + def request_origin + # this *needs* to be cached! + # otherwise you'd get different results if calling it more than once + @request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}" + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/renderer.rb b/actionpack/lib/action_controller/abstract/renderer.rb index a86eef889e..cd3e87d861 100644 --- a/actionpack/lib/action_controller/abstract/renderer.rb +++ b/actionpack/lib/action_controller/abstract/renderer.rb @@ -2,52 +2,63 @@ require "action_controller/abstract/logger" module AbstractController module Renderer + extend ActiveSupport::Concern + depends_on AbstractController::Logger - - setup do + + included do attr_internal :formats - + extlib_inheritable_accessor :_view_paths - + self._view_paths ||= ActionView::PathSet.new end - + def _action_view - @_action_view ||= ActionView::Base.new(self.class.view_paths, {}, self) + @_action_view ||= ActionView::Base.new(self.class.view_paths, {}, self) end - - def render(options = {}) - self.response_body = render_to_body(options) + + def render(*args) + if response_body + raise AbstractController::DoubleRenderError, "OMG" + end + + self.response_body = render_to_body(*args) end - + # Raw rendering of a template to a Rack-compatible body. # ==== # @option _prefix<String> The template's path prefix # @option _layout<String> The relative path to the layout template to use - # + # # :api: plugin def render_to_body(options = {}) - name = options[:_template_name] || action_name - - template = options[:_template] || view_paths.find_by_parts(name.to_s, formats, options[:_prefix]) - _render_template(template, options) + # TODO: Refactor so we can just use the normal template logic for this + if options[:_partial_object] + _action_view._render_partial_from_controller(options) + else + _determine_template(options) + _render_template(options) + end end # Raw rendering of a template to a string. # ==== # @option _prefix<String> The template's path prefix # @option _layout<String> The relative path to the layout template to use - # + # # :api: plugin def render_to_string(options = {}) AbstractController::Renderer.body_to_s(render_to_body(options)) end - def _render_template(template, options) - _action_view._render_template_with_layout(template) + def _render_template(options) + _action_view._render_template_from_controller(options[:_template], options[:_layout], options, options[:_partial]) + end + + def view_paths() + _view_paths end - - def view_paths() _view_paths end # Return a string representation of a Rack-compatible response body. def self.body_to_s(body) @@ -61,16 +72,28 @@ module AbstractController end end + private + def _determine_template(options) + name = (options[:_template_name] || action_name).to_s + + options[:_template] ||= view_paths.find_by_parts( + name, { :formats => formats }, options[:_prefix], options[:_partial] + ) + end + module ClassMethods - def append_view_path(path) self.view_paths << path end - + + def prepend_view_path(path) + self.view_paths.unshift(path) + end + def view_paths self._view_paths end - + def view_paths=(paths) self._view_paths = paths.is_a?(ActionView::PathSet) ? paths : ActionView::Base.process_view_paths(paths) diff --git a/actionpack/lib/action_controller/base/base.rb b/actionpack/lib/action_controller/base/base.rb index 3000b3d12f..67369eb122 100644 --- a/actionpack/lib/action_controller/base/base.rb +++ b/actionpack/lib/action_controller/base/base.rb @@ -1,5 +1,7 @@ require 'action_controller/deprecated' require 'set' +require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/module/attr_internal' module ActionController #:nodoc: class ActionControllerError < StandardError #:nodoc: @@ -30,10 +32,6 @@ module ActionController #:nodoc: def allowed_methods_header allowed_methods.map { |method_symbol| method_symbol.to_s.upcase } * ', ' end - - def handle_response!(response) - response.headers['Allow'] ||= allowed_methods_header - end end class NotImplemented < MethodNotAllowed #:nodoc: @@ -238,13 +236,12 @@ module ActionController #:nodoc: cattr_reader :protected_instance_variables # Controller specific instance variables which will not be accessible inside views. @@protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller - @action_name @before_filter_chain_aborted @action_cache_path @_session @_headers @_params + @action_name @before_filter_chain_aborted @action_cache_path @_headers @_params @_flash @_response) # Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets, # and images to a dedicated asset server away from the main web server. Example: # ActionController::Base.asset_host = "http://assets.example.com" - @@asset_host = "" cattr_accessor :asset_host # All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors. @@ -356,7 +353,9 @@ module ActionController #:nodoc: # Holds a hash of objects in the session. Accessed like <tt>session[:person]</tt> to get the object tied to the "person" # key. The session will hold any type of object as values, but the key should be a string or symbol. - attr_internal :session + def session + request.session + end # Holds a hash of header names and values. Accessed like <tt>headers["Cache-Control"]</tt> to get the value of the Cache-Control # directive. Values should always be specified as strings. @@ -365,19 +364,24 @@ module ActionController #:nodoc: # Returns the name of the action this controller is processing. attr_accessor :action_name - class << self - def call(env) - # HACK: For global rescue to have access to the original request and response - request = env["action_controller.rescue.request"] ||= ActionDispatch::Request.new(env) - response = env["action_controller.rescue.response"] ||= ActionDispatch::Response.new - process(request, response) - end + attr_reader :template - # Factory for the standard create, process loop where the controller is discarded after processing. - def process(request, response) #:nodoc: - new.process(request, response) - end + def action(name, env) + request = ActionDispatch::Request.new(env) + response = ActionDispatch::Response.new + self.action_name = name && name.to_s + process(request, response).to_a + end + + class << self + def action(name = nil) + @actions ||= {} + @actions[name] ||= proc do |env| + new.action(name, env) + end + end + # Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController". def controller_class_name @controller_class_name ||= name.demodulize @@ -443,60 +447,27 @@ module ActionController #:nodoc: @view_paths = superclass.view_paths.dup if @view_paths.nil? @view_paths.push(*path) end - - # Replace sensitive parameter data from the request log. - # Filters parameters that have any of the arguments as a substring. - # Looks in all subhashes of the param hash for keys to filter. - # If a block is given, each key and value of the parameter hash and all - # subhashes is passed to it, the value or key - # can be replaced using String#replace or similar method. - # - # Examples: - # filter_parameter_logging - # => Does nothing, just slows the logging process down - # - # filter_parameter_logging :password - # => replaces the value to all keys matching /password/i with "[FILTERED]" - # - # filter_parameter_logging :foo, "bar" - # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" - # - # filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i } - # => reverses the value to all keys matching /secret/i - # - # filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i } - # => reverses the value to all keys matching /secret/i, and - # replaces the value to all keys matching /foo|bar/i with "[FILTERED]" - def filter_parameter_logging(*filter_words, &block) - parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0 - - define_method(:filter_parameters) do |unfiltered_parameters| - filtered_parameters = {} - - unfiltered_parameters.each do |key, value| - if key =~ parameter_filter - filtered_parameters[key] = '[FILTERED]' - elsif value.is_a?(Hash) - filtered_parameters[key] = filter_parameters(value) - elsif block_given? - key = key.dup - value = value.dup if value - yield key, value - filtered_parameters[key] = value - else - filtered_parameters[key] = value - end - end - - filtered_parameters + + @@exempt_from_layout = [ActionView::TemplateHandlers::RJS] + + def exempt_from_layout(*types) + types.each do |type| + @@exempt_from_layout << + ActionView::Template.handler_class_for_extension(type) end - protected :filter_parameters + + @@exempt_from_layout end - delegate :exempt_from_layout, :to => 'ActionView::Template' end public + def call(env) + request = ActionDispatch::Request.new(env) + response = ActionDispatch::Response.new + process(request, response).to_a + end + # Extracts the action_name from the request parameters and performs that action. def process(request, response, method = :perform_action, *arguments) #:nodoc: response.request = request @@ -504,7 +475,6 @@ module ActionController #:nodoc: assign_shortcuts(request, response) initialize_template_class(response) initialize_current_url - assign_names log_processing send(method, *arguments) @@ -787,7 +757,6 @@ module ActionController #:nodoc: # Resets the session by clearing out all the objects stored within and initializing a new session object. def reset_session #:doc: request.reset_session - @_session = request.session end private @@ -804,20 +773,14 @@ module ActionController #:nodoc: end def initialize_template_class(response) - @template = response.template = ActionView::Base.new(self.class.view_paths, {}, self, formats) - response.template.helpers.send :include, self.class.master_helper_module - response.redirected_to = nil + @template = ActionView::Base.new(self.class.view_paths, {}, self, formats) + response.template = @template if response.respond_to?(:template=) + @template.helpers.send :include, self.class.master_helper_module @performed_render = @performed_redirect = false end def assign_shortcuts(request, response) - @_request, @_params = request, request.parameters - - @_response = response - @_response.session = request.session - - @_session = @_response.session - + @_request, @_response, @_params = request, response, request.parameters @_headers = @_response.headers end @@ -840,13 +803,6 @@ module ActionController #:nodoc: logger.info(request_id) end - def log_processing_for_parameters - parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup - parameters = parameters.except!(:controller, :action, :format, :_method) - - logger.info " Parameters: #{parameters.inspect}" unless parameters.empty? - end - def default_render #:nodoc: render end @@ -861,13 +817,13 @@ module ActionController #:nodoc: return (performed? ? ret : default_render) if called begin - default_render - rescue ActionView::MissingTemplate => e - raise e unless e.action_name == action_name - # If the path is the same as the action_name, the action is completely missing + view_paths.find_by_parts(action_name, {:formats => formats, :locales => [I18n.locale]}, controller_path) + rescue => e raise UnknownAction, "No action responded to #{action_name}. Actions: " + "#{action_methods.sort.to_sentence}", caller end + + default_render end # Returns true if a render or redirect has already been performed. @@ -875,10 +831,6 @@ module ActionController #:nodoc: @performed_render || @performed_redirect end - def assign_names - @action_name = (params['action'] || 'index') - end - def reset_variables_added_to_assigns @template.instance_variable_set("@assigns_added", nil) end @@ -894,10 +846,6 @@ module ActionController #:nodoc: "#{request.protocol}#{request.host}#{request.request_uri}" end - def close_session - # @_session.close if @_session && @_session.respond_to?(:close) - end - def default_template(action_name = self.action_name) self.view_paths.find_template(default_template_name(action_name), default_template_format) end @@ -921,7 +869,6 @@ module ActionController #:nodoc: end def process_cleanup - close_session end end @@ -929,7 +876,7 @@ module ActionController #:nodoc: [ Filters, Layout, Renderer, Redirector, Responder, Benchmarking, Rescue, Flash, MimeResponds, Helpers, Cookies, Caching, Verification, Streaming, SessionManagement, HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, RecordIdentifier, - RequestForgeryProtection, Translation + RequestForgeryProtection, Translation, FilterParameterLogging ].each do |mod| include mod end diff --git a/actionpack/lib/action_controller/base/chained/benchmarking.rb b/actionpack/lib/action_controller/base/chained/benchmarking.rb index 066150f58a..57a1ac8314 100644 --- a/actionpack/lib/action_controller/base/chained/benchmarking.rb +++ b/actionpack/lib/action_controller/base/chained/benchmarking.rb @@ -1,4 +1,4 @@ -require 'benchmark' +require 'active_support/core_ext/benchmark' module ActionController #:nodoc: # The benchmarking module times the performance of actions and reports to the logger. If the Active Record @@ -21,7 +21,7 @@ module ActionController #:nodoc: # easy to include benchmarking statements in production software that will remain inexpensive because the benchmark # will only be conducted if the log level is low enough. def benchmark(title, log_level = Logger::DEBUG, use_silence = true) - if logger && logger.level == log_level + if logger && logger.level >= log_level result = nil ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)") diff --git a/actionpack/lib/action_controller/base/chained/filters.rb b/actionpack/lib/action_controller/base/chained/filters.rb index 9022b8b279..e121c0129d 100644 --- a/actionpack/lib/action_controller/base/chained/filters.rb +++ b/actionpack/lib/action_controller/base/chained/filters.rb @@ -160,7 +160,7 @@ module ActionController #:nodoc: def convert_only_and_except_options_to_sets_of_strings(opts) [:only, :except].each do |key| if values = opts[key] - opts[key] = Array(values).map(&:to_s).to_set + opts[key] = Array(values).map {|val| val.to_s }.to_set end end end @@ -571,12 +571,7 @@ module ActionController #:nodoc: # Returns an array of Filter objects for this controller. def filter_chain - if chain = read_inheritable_attribute('filter_chain') - return chain - else - write_inheritable_attribute('filter_chain', FilterChain.new) - return filter_chain - end + read_inheritable_attribute('filter_chain') || write_inheritable_attribute('filter_chain', FilterChain.new) end # Returns all the before filters for this class and all its ancestors. diff --git a/actionpack/lib/action_controller/base/chained/flash.rb b/actionpack/lib/action_controller/base/chained/flash.rb index 56ee9c67e2..04d27bf090 100644 --- a/actionpack/lib/action_controller/base/chained/flash.rb +++ b/actionpack/lib/action_controller/base/chained/flash.rb @@ -26,9 +26,18 @@ module ActionController #:nodoc: # # See docs on the FlashHash class for more details about the flash. module Flash - def self.included(base) - base.class_eval do - include InstanceMethods + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + depends_on Session if defined?(ActionController::Http) + + included do + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + include InstanceMethodsForNewBase + else + include InstanceMethodsForBase + alias_method_chain :perform_action, :flash alias_method_chain :reset_session, :flash end @@ -120,44 +129,68 @@ module ActionController #:nodoc: (@used.keys - keys).each{ |k| @used.delete(k) } end + def store(session, key = "flash") + return if self.empty? + session[key] = self + end + private # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods # use() # marks the entire flash as used # use('msg') # marks the "msg" entry as used # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) - def use(k=nil, v=true) - unless k.nil? - @used[k] = v - else - keys.each{ |key| use(key, v) } - end + # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself + # if no key is passed. + def use(key = nil, used = true) + Array(key || keys).each { |k| @used[k] = used } + return key ? self[key] : self end end - module InstanceMethods #:nodoc: + module InstanceMethodsForBase #:nodoc: protected def perform_action_with_flash perform_action_without_flash - remove_instance_variable(:@_flash) if defined? @_flash + if defined? @_flash + @_flash.store(session) + remove_instance_variable(:@_flash) + end end def reset_session_with_flash reset_session_without_flash - remove_instance_variable(:@_flash) if defined? @_flash + remove_instance_variable(:@_flash) if defined?(@_flash) end + end - # Access the contents of the flash. Use <tt>flash["notice"]</tt> to - # read a notice you put there or <tt>flash["notice"] = "hello"</tt> - # to put a new one. - def flash #:doc: - unless defined? @_flash - @_flash = session["flash"] ||= FlashHash.new - @_flash.sweep + module InstanceMethodsForNewBase #:nodoc: + protected + def process_action(method_name) + super + if defined? @_flash + @_flash.store(session) + remove_instance_variable(:@_flash) end + end - @_flash + def reset_session + super + remove_instance_variable(:@_flash) if defined?(@_flash) end end + + protected + # Access the contents of the flash. Use <tt>flash["notice"]</tt> to + # read a notice you put there or <tt>flash["notice"] = "hello"</tt> + # to put a new one. + def flash #:doc: + if !defined?(@_flash) + @_flash = session["flash"] || FlashHash.new + @_flash.sweep + end + + @_flash + end end end diff --git a/actionpack/lib/action_controller/base/cookies.rb b/actionpack/lib/action_controller/base/cookies.rb index ca380e98d0..d4806623c3 100644 --- a/actionpack/lib/action_controller/base/cookies.rb +++ b/actionpack/lib/action_controller/base/cookies.rb @@ -51,7 +51,7 @@ module ActionController #:nodoc: protected # Returns the cookie container, which operates as described above. def cookies - CookieJar.new(self) + @cookies ||= CookieJar.new(self) end end diff --git a/actionpack/lib/action_controller/base/filter_parameter_logging.rb b/actionpack/lib/action_controller/base/filter_parameter_logging.rb new file mode 100644 index 0000000000..9df286ee24 --- /dev/null +++ b/actionpack/lib/action_controller/base/filter_parameter_logging.rb @@ -0,0 +1,97 @@ +module ActionController + module FilterParameterLogging + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + depends_on AbstractController::Logger + end + + included do + if defined?(ActionController::Http) + include InstanceMethodsForNewBase + end + end + + module ClassMethods + # Replace sensitive parameter data from the request log. + # Filters parameters that have any of the arguments as a substring. + # Looks in all subhashes of the param hash for keys to filter. + # If a block is given, each key and value of the parameter hash and all + # subhashes is passed to it, the value or key + # can be replaced using String#replace or similar method. + # + # Examples: + # filter_parameter_logging + # => Does nothing, just slows the logging process down + # + # filter_parameter_logging :password + # => replaces the value to all keys matching /password/i with "[FILTERED]" + # + # filter_parameter_logging :foo, "bar" + # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + # + # filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i } + # => reverses the value to all keys matching /secret/i + # + # filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i } + # => reverses the value to all keys matching /secret/i, and + # replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + def filter_parameter_logging(*filter_words, &block) + parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0 + + define_method(:filter_parameters) do |unfiltered_parameters| + filtered_parameters = {} + + unfiltered_parameters.each do |key, value| + if key =~ parameter_filter + filtered_parameters[key] = '[FILTERED]' + elsif value.is_a?(Hash) + filtered_parameters[key] = filter_parameters(value) + elsif block_given? + key = key.dup + value = value.dup if value + yield key, value + filtered_parameters[key] = value + else + filtered_parameters[key] = value + end + end + + filtered_parameters + end + protected :filter_parameters + end + end + + module InstanceMethodsForNewBase + # TODO : Fix the order of information inside such that it's exactly same as the old base + def process(*) + ret = super + + if logger + parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup + parameters = parameters.except!(:controller, :action, :format, :_method, :only_path) + + unless parameters.empty? + # TODO : Move DelayedLog to AS + log = AbstractController::Logger::DelayedLog.new { " Parameters: #{parameters.inspect}" } + logger.info(log) + end + end + + ret + end + end + + private + + # TODO : This method is not needed for the new base + def log_processing_for_parameters + parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup + parameters = parameters.except!(:controller, :action, :format, :_method) + + logger.info " Parameters: #{parameters.inspect}" unless parameters.empty? + end + end +end diff --git a/actionpack/lib/action_controller/base/helpers.rb b/actionpack/lib/action_controller/base/helpers.rb index ba65032f6a..f74158bc13 100644 --- a/actionpack/lib/action_controller/base/helpers.rb +++ b/actionpack/lib/action_controller/base/helpers.rb @@ -3,23 +3,19 @@ require 'active_support/dependencies' # FIXME: helper { ... } is broken on Ruby 1.9 module ActionController #:nodoc: module Helpers #:nodoc: - def self.included(base) + extend ActiveSupport::Concern + + included do # Initialize the base module to aggregate its helpers. - base.class_inheritable_accessor :master_helper_module - base.master_helper_module = Module.new + class_inheritable_accessor :master_helper_module + self.master_helper_module = Module.new # Set the default directory for helpers - base.class_inheritable_accessor :helpers_dir - base.helpers_dir = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers") - - # Extend base with class methods to declare helpers. - base.extend(ClassMethods) + class_inheritable_accessor :helpers_dir + self.helpers_dir = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers") - base.class_eval do - # Wrap inherited to create a new master helper module for subclasses. - class << self - alias_method_chain :inherited, :helper - end + class << self + alias_method_chain :inherited, :helper end end diff --git a/actionpack/lib/action_controller/base/http_authentication.rb b/actionpack/lib/action_controller/base/http_authentication.rb index b6b5267c66..2893290efb 100644 --- a/actionpack/lib/action_controller/base/http_authentication.rb +++ b/actionpack/lib/action_controller/base/http_authentication.rb @@ -1,3 +1,5 @@ +require 'active_support/base64' + module ActionController module HttpAuthentication # Makes it dead easy to do HTTP Basic authentication. @@ -192,9 +194,10 @@ module ActionController if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque] password = password_procedure.call(credentials[:username]) + method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] [true, false].any? do |password_is_ha1| - expected = expected_response(request.env['REQUEST_METHOD'], request.env['REQUEST_URI'], credentials, password, password_is_ha1) + expected = expected_response(method, request.env['REQUEST_URI'], credentials, password, password_is_ha1) expected == credentials[:response] end end @@ -276,7 +279,7 @@ module ActionController t = time.to_i hashed = [t, secret_key] digest = ::Digest::MD5.hexdigest(hashed.join(":")) - Base64.encode64("#{t}:#{digest}").gsub("\n", '') + ActiveSupport::Base64.encode64("#{t}:#{digest}").gsub("\n", '') end # Might want a shorter timeout depending on whether the request @@ -285,7 +288,7 @@ module ActionController # allow a user to use new nonce without prompting user again for their # username and password. def validate_nonce(request, value, seconds_to_timeout=5*60) - t = Base64.decode64(value).split(":").first.to_i + t = ActiveSupport::Base64.decode64(value).split(":").first.to_i nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end diff --git a/actionpack/lib/action_controller/base/layout.rb b/actionpack/lib/action_controller/base/layout.rb index 4fcef6c5d9..cf5f46a32b 100644 --- a/actionpack/lib/action_controller/base/layout.rb +++ b/actionpack/lib/action_controller/base/layout.rb @@ -1,3 +1,7 @@ +require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/class/delegating_attributes' +require 'active_support/core_ext/class/inheritable_attributes' + module ActionController #:nodoc: module Layout #:nodoc: def self.included(base) @@ -182,7 +186,7 @@ module ActionController #:nodoc: def memoized_find_layout(layout, formats) #:nodoc: return layout if layout.nil? || layout.respond_to?(:render) prefix = layout.to_s =~ /layouts\// ? nil : "layouts" - view_paths.find_by_parts(layout.to_s, formats, prefix) + view_paths.find_by_parts(layout.to_s, {:formats => formats}, prefix) end def find_layout(*args) diff --git a/actionpack/lib/action_controller/base/mime_responds.rb b/actionpack/lib/action_controller/base/mime_responds.rb index bac225ab2a..3c17dda1a1 100644 --- a/actionpack/lib/action_controller/base/mime_responds.rb +++ b/actionpack/lib/action_controller/base/mime_responds.rb @@ -1,111 +1,103 @@ module ActionController #:nodoc: module MimeResponds #:nodoc: - def self.included(base) - base.module_eval do - include ActionController::MimeResponds::InstanceMethods - end - end - - module InstanceMethods - # Without web-service support, an action which collects the data for displaying a list of people - # might look something like this: - # - # def index - # @people = Person.find(:all) - # end - # - # Here's the same action, with web-service support baked in: - # - # def index - # @people = Person.find(:all) - # - # respond_to do |format| - # format.html - # format.xml { render :xml => @people.to_xml } - # end - # end - # - # What that says is, "if the client wants HTML in response to this action, just respond as we - # would have before, but if the client wants XML, return them the list of people in XML format." - # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) - # - # Supposing you have an action that adds a new person, optionally creating their company - # (by name) if it does not already exist, without web-services, it might look like this: - # - # def create - # @company = Company.find_or_create_by_name(params[:company][:name]) - # @person = @company.people.create(params[:person]) - # - # redirect_to(person_list_url) - # end - # - # Here's the same action, with web-service support baked in: - # - # def create - # company = params[:person].delete(:company) - # @company = Company.find_or_create_by_name(company[:name]) - # @person = @company.people.create(params[:person]) - # - # respond_to do |format| - # format.html { redirect_to(person_list_url) } - # format.js - # format.xml { render :xml => @person.to_xml(:include => @company) } - # end - # end - # - # If the client wants HTML, we just redirect them back to the person list. If they want Javascript - # (format.js), then it is an RJS request and we render the RJS template associated with this action. - # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also - # include the person's company in the rendered XML, so you get something like this: - # - # <person> - # <id>...</id> - # ... - # <company> - # <id>...</id> - # <name>...</name> - # ... - # </company> - # </person> - # - # Note, however, the extra bit at the top of that action: - # - # company = params[:person].delete(:company) - # @company = Company.find_or_create_by_name(company[:name]) - # - # This is because the incoming XML document (if a web-service request is in process) can only contain a - # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): - # - # person[name]=...&person[company][name]=...&... - # - # And, like this (xml-encoded): - # - # <person> - # <name>...</name> - # <company> - # <name>...</name> - # </company> - # </person> - # - # In other words, we make the request so that it operates on a single entity's person. Then, in the action, - # we extract the company data from the request, find or create the company, and then create the new person - # with the remaining data. - # - # Note that you can define your own XML parameter parser which would allow you to describe multiple entities - # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow - # and accept Rails' defaults, life will be much easier. - # - # If you need to use a MIME type which isn't supported by default, you can register your own handlers in - # environment.rb as follows. - # - # Mime::Type.register "image/jpg", :jpg - def respond_to(*types, &block) - raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block - block ||= lambda { |responder| types.each { |type| responder.send(type) } } - responder = Responder.new(self) - block.call(responder) - responder.respond - end + # Without web-service support, an action which collects the data for displaying a list of people + # might look something like this: + # + # def index + # @people = Person.find(:all) + # end + # + # Here's the same action, with web-service support baked in: + # + # def index + # @people = Person.find(:all) + # + # respond_to do |format| + # format.html + # format.xml { render :xml => @people.to_xml } + # end + # end + # + # What that says is, "if the client wants HTML in response to this action, just respond as we + # would have before, but if the client wants XML, return them the list of people in XML format." + # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) + # + # Supposing you have an action that adds a new person, optionally creating their company + # (by name) if it does not already exist, without web-services, it might look like this: + # + # def create + # @company = Company.find_or_create_by_name(params[:company][:name]) + # @person = @company.people.create(params[:person]) + # + # redirect_to(person_list_url) + # end + # + # Here's the same action, with web-service support baked in: + # + # def create + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # @person = @company.people.create(params[:person]) + # + # respond_to do |format| + # format.html { redirect_to(person_list_url) } + # format.js + # format.xml { render :xml => @person.to_xml(:include => @company) } + # end + # end + # + # If the client wants HTML, we just redirect them back to the person list. If they want Javascript + # (format.js), then it is an RJS request and we render the RJS template associated with this action. + # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also + # include the person's company in the rendered XML, so you get something like this: + # + # <person> + # <id>...</id> + # ... + # <company> + # <id>...</id> + # <name>...</name> + # ... + # </company> + # </person> + # + # Note, however, the extra bit at the top of that action: + # + # company = params[:person].delete(:company) + # @company = Company.find_or_create_by_name(company[:name]) + # + # This is because the incoming XML document (if a web-service request is in process) can only contain a + # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): + # + # person[name]=...&person[company][name]=...&... + # + # And, like this (xml-encoded): + # + # <person> + # <name>...</name> + # <company> + # <name>...</name> + # </company> + # </person> + # + # In other words, we make the request so that it operates on a single entity's person. Then, in the action, + # we extract the company data from the request, find or create the company, and then create the new person + # with the remaining data. + # + # Note that you can define your own XML parameter parser which would allow you to describe multiple entities + # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow + # and accept Rails' defaults, life will be much easier. + # + # If you need to use a MIME type which isn't supported by default, you can register your own handlers in + # environment.rb as follows. + # + # Mime::Type.register "image/jpg", :jpg + def respond_to(*types, &block) + raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block + block ||= lambda { |responder| types.each { |type| responder.send(type) } } + responder = Responder.new(self) + block.call(responder) + responder.respond end class Responder #:nodoc: @@ -127,8 +119,14 @@ module ActionController #:nodoc: @order << mime_type @responses[mime_type] ||= Proc.new do - @response.template.formats = [mime_type.to_sym] + # TODO: Remove this when new base is merged in + if defined?(Http) + @controller.formats = [mime_type.to_sym] + end + + @controller.template.formats = [mime_type.to_sym] @response.content_type = mime_type.to_s + block_given? ? block.call : @controller.send(:render, :action => @controller.action_name) end end diff --git a/actionpack/lib/action_controller/base/redirect.rb b/actionpack/lib/action_controller/base/redirect.rb index 2e92117e7c..7e10f614e2 100644 --- a/actionpack/lib/action_controller/base/redirect.rb +++ b/actionpack/lib/action_controller/base/redirect.rb @@ -48,8 +48,6 @@ module ActionController status = 302 end - response.redirected_to = options - case options # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") @@ -72,7 +70,9 @@ module ActionController def redirect_to_full_url(url, status) raise DoubleRenderError if performed? logger.info("Redirected to #{url}") if logger && logger.info? - response.redirect(url, interpret_status(status)) + response.status = interpret_status(status) + response.location = url.gsub(/[\r\n]/, '') + response.body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>" @performed_redirect = true end @@ -82,8 +82,6 @@ module ActionController # The response body is not reset here, see +erase_render_results+ def erase_redirect_results #:nodoc: @performed_redirect = false - response.redirected_to = nil - response.redirected_to_method_params = nil response.status = DEFAULT_RENDER_STATUS_CODE response.headers.delete('Location') end diff --git a/actionpack/lib/action_controller/base/render.rb b/actionpack/lib/action_controller/base/render.rb index 606df58518..cc0d878e01 100644 --- a/actionpack/lib/action_controller/base/render.rb +++ b/actionpack/lib/action_controller/base/render.rb @@ -253,8 +253,9 @@ module ActionController response.content_type ||= Mime::JS render_for_text(js) - elsif json = options[:json] - json = json.to_json unless json.is_a?(String) + elsif options.include?(:json) + json = options[:json] + json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? response.content_type ||= Mime::JSON render_for_text(json) @@ -374,12 +375,18 @@ module ActionController render_for_file(name.sub(/^\//, ''), [layout, true], options) end end - - def render_for_parts(parts, layout, options = {}) + + # ==== Arguments + # parts<Array[String, Array[Symbol*], String, Boolean]>:: + # Example: ["show", [:html, :xml], "users", false] + def render_for_parts(parts, layout_details, options = {}) + parts[1] = {:formats => parts[1], :locales => [I18n.locale]} + tmp = view_paths.find_by_parts(*parts) - layout = _pick_layout(*layout) unless tmp.exempt_from_layout? - + layout = _pick_layout(*layout_details) unless + self.class.exempt_from_layout.include?(tmp.handler) + render_for_text( @template._render_template_with_layout(tmp, layout, options, parts[3])) end diff --git a/actionpack/lib/action_controller/base/request_forgery_protection.rb b/actionpack/lib/action_controller/base/request_forgery_protection.rb index 3067122ceb..368c6e9de8 100644 --- a/actionpack/lib/action_controller/base/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/base/request_forgery_protection.rb @@ -3,12 +3,26 @@ module ActionController #:nodoc: end module RequestForgeryProtection - def self.included(base) - base.class_eval do - helper_method :form_authenticity_token - helper_method :protect_against_forgery? + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + depends_on AbstractController::Helpers, Session + end + + included do + if defined?(ActionController::Http) + # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+ + # sets it to <tt>:authenticity_token</tt> by default. + cattr_accessor :request_forgery_protection_token + + # Controls whether request forgergy protection is turned on or not. Turned off by default only in test mode. + class_inheritable_accessor :allow_forgery_protection + self.allow_forgery_protection = true end - base.extend(ClassMethods) + + helper_method :form_authenticity_token + helper_method :protect_against_forgery? end # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current web application, not a diff --git a/actionpack/lib/action_controller/base/rescue.rb b/actionpack/lib/action_controller/base/rescue.rb new file mode 100644 index 0000000000..2717a06a37 --- /dev/null +++ b/actionpack/lib/action_controller/base/rescue.rb @@ -0,0 +1,50 @@ +module ActionController #:nodoc: + # Actions that fail to perform as expected throw exceptions. These + # exceptions can either be rescued for the public view (with a nice + # user-friendly explanation) or for the developers view (with tons of + # debugging information). The developers view is already implemented by + # the Action Controller, but the public view should be tailored to your + # specific application. + # + # The default behavior for public exceptions is to render a static html + # file with the name of the error code thrown. If no such file exists, an + # empty response is sent with the correct status code. + # + # You can override what constitutes a local request by overriding the + # <tt>local_request?</tt> method in your own controller. Custom rescue + # behavior is achieved by overriding the <tt>rescue_action_in_public</tt> + # and <tt>rescue_action_locally</tt> methods. + module Rescue + def self.included(base) #:nodoc: + base.send :include, ActiveSupport::Rescuable + base.extend(ClassMethods) + + base.class_eval do + alias_method_chain :perform_action, :rescue + end + end + + module ClassMethods + def rescue_action(env) + exception = env.delete('action_dispatch.rescue.exception') + request = ActionDispatch::Request.new(env) + response = ActionDispatch::Response.new + new.process(request, response, :rescue_action, exception).to_a + end + end + + protected + # Exception handler called when the performance of an action raises + # an exception. + def rescue_action(exception) + rescue_with_handler(exception) || raise(exception) + end + + private + def perform_action_with_rescue + perform_action_without_rescue + rescue Exception => exception + rescue_action(exception) + end + end +end diff --git a/actionpack/lib/action_controller/base/streaming.rb b/actionpack/lib/action_controller/base/streaming.rb index 9f80f48c3d..5f56c95483 100644 --- a/actionpack/lib/action_controller/base/streaming.rb +++ b/actionpack/lib/action_controller/base/streaming.rb @@ -2,6 +2,13 @@ module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, # instead of rendering. module Streaming + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + depends_on ActionController::Renderer + end + DEFAULT_SEND_FILE_OPTIONS = { :type => 'application/octet-stream'.freeze, :disposition => 'attachment'.freeze, @@ -88,6 +95,7 @@ module ActionController #:nodoc: head options[:status], X_SENDFILE_HEADER => path else if options[:stream] + # TODO : Make render :text => proc {} work with the new base render :status => options[:status], :text => Proc.new { |response, output| logger.info "Streaming file #{path}" unless logger.nil? len = options[:buffer_size] || 4096 diff --git a/actionpack/lib/action_controller/base/verification.rb b/actionpack/lib/action_controller/base/verification.rb index c62b81b666..31654e36f3 100644 --- a/actionpack/lib/action_controller/base/verification.rb +++ b/actionpack/lib/action_controller/base/verification.rb @@ -1,7 +1,10 @@ module ActionController #:nodoc: module Verification #:nodoc: - def self.included(base) #:nodoc: - base.extend(ClassMethods) + extend ActiveSupport::Concern + + # TODO : Remove the defined? check when new base is the main base + if defined?(ActionController::Http) + depends_on AbstractController::Callbacks, Session, Flash, Renderer end # This module provides a class-level method for specifying that certain @@ -102,7 +105,7 @@ module ActionController #:nodoc: end def verify_presence_of_keys_in_hash_flash_or_params(options) # :nodoc: - [*options[:params] ].find { |v| params[v].nil? } || + [*options[:params] ].find { |v| v && params[v.to_sym].nil? } || [*options[:session]].find { |v| session[v].nil? } || [*options[:flash] ].find { |v| flash[v].nil? } end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index ffd8081edc..38cf1da6a8 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -24,31 +24,31 @@ module ActionController #:nodoc: # ActionController::Base.cache_store = :mem_cache_store, "localhost" # ActionController::Base.cache_store = MyOwnStore.new("parameter") module Caching + extend ActiveSupport::Concern + autoload :Actions, 'action_controller/caching/actions' autoload :Fragments, 'action_controller/caching/fragments' autoload :Pages, 'action_controller/caching/pages' autoload :Sweeper, 'action_controller/caching/sweeping' autoload :Sweeping, 'action_controller/caching/sweeping' - def self.included(base) #:nodoc: - base.class_eval do - @@cache_store = nil - cattr_reader :cache_store + included do + @@cache_store = nil + cattr_reader :cache_store - # Defines the storage option for cached fragments - def self.cache_store=(store_option) - @@cache_store = ActiveSupport::Cache.lookup_store(store_option) - end + # Defines the storage option for cached fragments + def self.cache_store=(store_option) + @@cache_store = ActiveSupport::Cache.lookup_store(store_option) + end - include Pages, Actions, Fragments - include Sweeping if defined?(ActiveRecord) + include Pages, Actions, Fragments + include Sweeping if defined?(ActiveRecord) - @@perform_caching = true - cattr_accessor :perform_caching + @@perform_caching = true + cattr_accessor :perform_caching - def self.cache_configured? - perform_caching && cache_store - end + def self.cache_configured? + perform_caching && cache_store end end diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index b99feddf77..54148b55d8 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -61,7 +61,15 @@ module ActionController #:nodoc: filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) } cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options) - around_filter(cache_filter, filter_options) + + # TODO: Remove this once new base is swapped in. + if defined?(ActionController::Http) + around_filter cache_filter, filter_options + else + around_filter(filter_options) do |controller, action| + cache_filter.filter(controller, action) + end + end end end @@ -83,8 +91,24 @@ module ActionController #:nodoc: @options = options end + # TODO: Remove once New Base is merged + if defined?(ActionController::Http) + def filter(controller) + should_continue = before(controller) + yield if should_continue + after(controller) + end + else + def filter(controller, action) + should_continue = before(controller) + action.call if should_continue + after(controller) + end + end + def before(controller) cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path))) + if cache = controller.read_fragment(cache_path.path, @options[:store_options]) controller.rendered_action_cache = true set_content_type!(controller, cache_path.extension) @@ -121,7 +145,9 @@ module ActionController #:nodoc: end def content_for_layout(controller) - controller.response.layout && controller.response.template.instance_variable_get('@cached_content_for_layout') + # TODO: Remove this when new base is merged in + template = controller.respond_to?(:template) ? controller.template : controller._action_view + template.layout && template.instance_variable_get('@cached_content_for_layout') end end diff --git a/actionpack/lib/action_controller/dispatch/dispatcher.rb b/actionpack/lib/action_controller/dispatch/dispatcher.rb index bb9d8bd063..9ad1cadfd3 100644 --- a/actionpack/lib/action_controller/dispatch/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatch/dispatcher.rb @@ -1,96 +1,65 @@ +require 'active_support/core_ext/module/delegation' + module ActionController # Dispatches requests to the appropriate controller and takes care of # reloading the app after each request when Dependencies.load? is true. class Dispatcher + cattr_accessor :prepare_each_request + self.prepare_each_request = false + + cattr_accessor :router + self.router = Routing::Routes + + cattr_accessor :middleware + self.middleware = ActionDispatch::MiddlewareStack.new do |middleware| + middlewares = File.join(File.dirname(__FILE__), "middlewares.rb") + middleware.instance_eval(File.read(middlewares), middlewares, 1) + end + class << self def define_dispatcher_callbacks(cache_classes) unless cache_classes - unless self.middleware.include?(ActionDispatch::Reloader) - self.middleware.insert_after(ActionDispatch::Failsafe, ActionDispatch::Reloader) + # Run prepare callbacks before every request in development mode + self.prepare_each_request = true + + # Development mode callbacks + ActionDispatch::Callbacks.before_dispatch do |app| + ActionController::Dispatcher.router.reload + end + + ActionDispatch::Callbacks.after_dispatch do + # Cleanup the application before processing the current request. + ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord) + ActiveSupport::Dependencies.clear + ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord) end ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false end if defined?(ActiveRecord) - to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers } + to_prepare(:activerecord_instantiate_observers) do + ActiveRecord::Base.instantiate_observers + end end - after_dispatch :flush_logger if Base.logger && Base.logger.respond_to?(:flush) + if Base.logger && Base.logger.respond_to?(:flush) + after_dispatch do + Base.logger.flush + end + end to_prepare do I18n.reload! end end - # Add a preparation callback. Preparation callbacks are run before every - # request in development mode, and before the first request in production - # mode. - # - # An optional identifier may be supplied for the callback. If provided, - # to_prepare may be called again with the same identifier to replace the - # existing callback. Passing an identifier is a suggested practice if the - # code adding a preparation block may be reloaded. - def to_prepare(identifier = nil, &block) - @prepare_dispatch_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new - callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier) - @prepare_dispatch_callbacks.replace_or_append!(callback) - end - - def run_prepare_callbacks - new.send :run_callbacks, :prepare_dispatch - end - - def reload_application - # Run prepare callbacks before every request in development mode - run_prepare_callbacks + delegate :to_prepare, :prepare_dispatch, :before_dispatch, :after_dispatch, + :to => ActionDispatch::Callbacks - Routing::Routes.reload + def new + @@middleware.build(@@router) end - - def cleanup_application - # Cleanup the application before processing the current request. - ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord) - ActiveSupport::Dependencies.clear - ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord) - end - end - - cattr_accessor :middleware - self.middleware = ActionDispatch::MiddlewareStack.new do |middleware| - middlewares = File.join(File.dirname(__FILE__), "middlewares.rb") - middleware.instance_eval(File.read(middlewares)) - end - - include ActiveSupport::Callbacks - define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch - - def initialize - @app = @@middleware.build(lambda { |env| self._call(env) }) - freeze - end - - def call(env) - @app.call(env) - end - - def _call(env) - begin - run_callbacks :before_dispatch - Routing::Routes.call(env) - rescue Exception => exception - if controller ||= (::ApplicationController rescue Base) - controller.call_with_exception(env, exception).to_a - else - raise exception - end - ensure - run_callbacks :after_dispatch, :enumerator => :reverse_each - end - end - - def flush_logger - Base.logger.flush end end end diff --git a/actionpack/lib/action_controller/dispatch/middlewares.rb b/actionpack/lib/action_controller/dispatch/middlewares.rb index b62b4f84a1..b25ed3fd3f 100644 --- a/actionpack/lib/action_controller/dispatch/middlewares.rb +++ b/actionpack/lib/action_controller/dispatch/middlewares.rb @@ -2,12 +2,17 @@ use "Rack::Lock", :if => lambda { !ActionController::Base.allow_concurrency } -use "ActionDispatch::Failsafe" +use "ActionDispatch::ShowExceptions", lambda { ActionController::Base.consider_all_requests_local } +use "ActionDispatch::Callbacks", lambda { ActionController::Dispatcher.prepare_each_request } +use "ActionDispatch::Rescue", lambda { + controller = (::ApplicationController rescue ActionController::Base) + # TODO: Replace with controller.action(:_rescue_action) + controller.method(:rescue_action) +} use lambda { ActionController::Base.session_store }, lambda { ActionController::Base.session_options } -use "ActionDispatch::RewindableInput" use "ActionDispatch::ParamsParser" use "Rack::MethodOverride" -use "Rack::Head" +use "Rack::Head"
\ No newline at end of file diff --git a/actionpack/lib/action_controller/dispatch/rescue.rb b/actionpack/lib/action_controller/dispatch/rescue.rb deleted file mode 100644 index df80ac0909..0000000000 --- a/actionpack/lib/action_controller/dispatch/rescue.rb +++ /dev/null @@ -1,185 +0,0 @@ -module ActionController #:nodoc: - # Actions that fail to perform as expected throw exceptions. These - # exceptions can either be rescued for the public view (with a nice - # user-friendly explanation) or for the developers view (with tons of - # debugging information). The developers view is already implemented by - # the Action Controller, but the public view should be tailored to your - # specific application. - # - # The default behavior for public exceptions is to render a static html - # file with the name of the error code thrown. If no such file exists, an - # empty response is sent with the correct status code. - # - # You can override what constitutes a local request by overriding the - # <tt>local_request?</tt> method in your own controller. Custom rescue - # behavior is achieved by overriding the <tt>rescue_action_in_public</tt> - # and <tt>rescue_action_locally</tt> methods. - module Rescue - LOCALHOST = '127.0.0.1'.freeze - - DEFAULT_RESCUE_RESPONSE = :internal_server_error - DEFAULT_RESCUE_RESPONSES = { - 'ActionController::RoutingError' => :not_found, - 'ActionController::UnknownAction' => :not_found, - 'ActiveRecord::RecordNotFound' => :not_found, - 'ActiveRecord::StaleObjectError' => :conflict, - 'ActiveRecord::RecordInvalid' => :unprocessable_entity, - 'ActiveRecord::RecordNotSaved' => :unprocessable_entity, - 'ActionController::MethodNotAllowed' => :method_not_allowed, - 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity - } - - DEFAULT_RESCUE_TEMPLATE = 'diagnostics' - DEFAULT_RESCUE_TEMPLATES = { - 'ActionView::MissingTemplate' => 'missing_template', - 'ActionController::RoutingError' => 'routing_error', - 'ActionController::UnknownAction' => 'unknown_action', - 'ActionView::TemplateError' => 'template_error' - } - - RESCUES_TEMPLATE_PATH = ActionView::Template::FileSystemPath.new( - File.join(File.dirname(__FILE__), "templates")) - - def self.included(base) #:nodoc: - base.cattr_accessor :rescue_responses - base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE) - base.rescue_responses.update DEFAULT_RESCUE_RESPONSES - - base.cattr_accessor :rescue_templates - base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE) - base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES - - base.extend(ClassMethods) - base.send :include, ActiveSupport::Rescuable - - base.class_eval do - alias_method_chain :perform_action, :rescue - end - end - - module ClassMethods - def call_with_exception(env, exception) #:nodoc: - request = env["action_controller.rescue.request"] ||= ActionDispatch::Request.new(env) - response = env["action_controller.rescue.response"] ||= ActionDispatch::Response.new - new.process(request, response, :rescue_action, exception) - end - end - - protected - # Exception handler called when the performance of an action raises - # an exception. - def rescue_action(exception) - rescue_with_handler(exception) || - rescue_action_without_handler(exception) - end - - # Overwrite to implement custom logging of errors. By default - # logs as fatal. - def log_error(exception) #:doc: - ActiveSupport::Deprecation.silence do - if ActionView::TemplateError === exception - logger.fatal(exception.to_s) - else - logger.fatal( - "\n#{exception.class} (#{exception.message}):\n " + - clean_backtrace(exception).join("\n ") + "\n\n" - ) - end - end - end - - # Overwrite to implement public exception handling (for requests - # answering false to <tt>local_request?</tt>). By default will call - # render_optional_error_file. Override this method to provide more - # user friendly error messages. - def rescue_action_in_public(exception) #:doc: - render_optional_error_file response_code_for_rescue(exception) - end - - # Attempts to render a static error page based on the - # <tt>status_code</tt> thrown, or just return headers if no such file - # exists. At first, it will try to render a localized static page. - # For example, if a 500 error is being handled Rails and locale is :da, - # it will first attempt to render the file at <tt>public/500.da.html</tt> - # then attempt to render <tt>public/500.html</tt>. If none of them exist, - # the body of the response will be left empty. - def render_optional_error_file(status_code) - status = interpret_status(status_code) - locale_path = "#{Rails.public_path}/#{status[0,3]}.#{I18n.locale}.html" if I18n.locale - path = "#{Rails.public_path}/#{status[0,3]}.html" - - if locale_path && File.exist?(locale_path) - render :file => locale_path, :status => status, :content_type => Mime::HTML - elsif File.exist?(path) - render :file => path, :status => status, :content_type => Mime::HTML - else - head status - end - end - - # True if the request came from localhost, 127.0.0.1. Override this - # method if you wish to redefine the meaning of a local request to - # include remote IP addresses or other criteria. - def local_request? #:doc: - request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST - end - - # Render detailed diagnostics for unhandled exceptions rescued from - # a controller action. - def rescue_action_locally(exception) - @template.instance_variable_set("@exception", exception) - @template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH) - @template.instance_variable_set("@contents", - @template._render_template(template_path_for_local_rescue(exception))) - - response.content_type = Mime::HTML - response.status = interpret_status(response_code_for_rescue(exception)) - - content = @template._render_template(rescues_path("layout")) - render_for_text(content) - end - - def rescue_action_without_handler(exception) - log_error(exception) if logger - erase_results if performed? - - # Let the exception alter the response if it wants. - # For example, MethodNotAllowed sets the Allow header. - if exception.respond_to?(:handle_response!) - exception.handle_response!(response) - end - - if consider_all_requests_local || local_request? - rescue_action_locally(exception) - else - rescue_action_in_public(exception) - end - end - - private - def perform_action_with_rescue #:nodoc: - perform_action_without_rescue - rescue Exception => exception - rescue_action(exception) - end - - def rescues_path(template_name) - RESCUES_TEMPLATE_PATH.find_by_parts("rescues/#{template_name}.erb") - end - - def template_path_for_local_rescue(exception) - rescues_path(rescue_templates[exception.class.name]) - end - - def response_code_for_rescue(exception) - rescue_responses[exception.class.name] - end - - def clean_backtrace(exception) - defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? - Rails.backtrace_cleaner.clean(exception.backtrace) : - exception.backtrace - end - end -end diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb deleted file mode 100644 index 64b34650b1..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb +++ /dev/null @@ -1,24 +0,0 @@ -<% unless @exception.blamed_files.blank? %> - <% if (hide = @exception.blamed_files.length > 8) %> - <a href="#" onclick="document.getElementById('blame_trace').style.display='block'; return false;">Show blamed files</a> - <% end %> - <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%=h @exception.describe_blame %></code></pre> -<% end %> - -<% - clean_params = request.parameters.clone - clean_params.delete("action") - clean_params.delete("controller") - - request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") -%> - -<h2 style="margin-top: 30px">Request</h2> -<p><b>Parameters</b>: <pre><%=h request_dump %></pre></p> - -<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p> -<div id="session_dump" style="display:none"><%= debug(request.session.instance_variable_get("@data")) %></div> - - -<h2 style="margin-top: 30px">Response</h2> -<p><b>Headers</b>: <pre><%=h response ? response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb deleted file mode 100644 index bb2d8375bd..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb +++ /dev/null @@ -1,26 +0,0 @@ -<% - traces = [ - ["Application Trace", @exception.application_backtrace], - ["Framework Trace", @exception.framework_backtrace], - ["Full Trace", @exception.clean_backtrace] - ] - names = traces.collect {|name, trace| name} -%> - -<p><code>RAILS_ROOT: <%= defined?(RAILS_ROOT) ? RAILS_ROOT : "unset" %></code></p> - -<div id="traces"> - <% names.each do |name| %> - <% - show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';" - hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"} - %> - <a href="#" onclick="<%= hide %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> - <% end %> - - <% traces.each do |name, trace| %> - <div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;"> - <pre><code><%= trace.join "\n" %></code></pre> - </div> - <% end %> -</div> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb deleted file mode 100644 index e5c647c826..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb +++ /dev/null @@ -1,10 +0,0 @@ -<h1> - <%=h @exception.class.to_s %> - <% if request.parameters['controller'] %> - in <%=h request.parameters['controller'].humanize %>Controller<% if request.parameters['action'] %>#<%=h request.parameters['action'] %><% end %> - <% end %> -</h1> -<pre><%=h @exception.clean_message %></pre> - -<%= @template._render_template(@rescues_path.find_by_parts("rescues/_trace.erb")) %> -<%= @template._render_template(@rescues_path.find_by_parts("rescues/_request_and_response.erb")) %>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb deleted file mode 100644 index 4a04742e40..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb +++ /dev/null @@ -1,29 +0,0 @@ -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> - <title>Action Controller: Exception caught</title> - <style> - body { background-color: #fff; color: #333; } - - body, p, ol, ul, td { - font-family: verdana, arial, helvetica, sans-serif; - font-size: 13px; - line-height: 18px; - } - - pre { - background-color: #eee; - padding: 10px; - font-size: 11px; - } - - a { color: #000; } - a:visited { color: #666; } - a:hover { color: #fff; background-color:#000; } - </style> -</head> -<body> - -<%= @contents %> - -</body> -</html>
\ No newline at end of file diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb deleted file mode 100644 index dbfdf76947..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb +++ /dev/null @@ -1,2 +0,0 @@ -<h1>Template is missing</h1> -<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb deleted file mode 100644 index ccfa858cce..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb +++ /dev/null @@ -1,10 +0,0 @@ -<h1>Routing Error</h1> -<p><pre><%=h @exception.message %></pre></p> -<% unless @exception.failures.empty? %><p> - <h2>Failure reasons:</h2> - <ol> - <% @exception.failures.each do |route, reason| %> - <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li> - <% end %> - </ol> -</p><% end %> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb deleted file mode 100644 index 2e34e03bd5..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb +++ /dev/null @@ -1,21 +0,0 @@ -<h1> - <%=h @exception.original_exception.class.to_s %> in - <%=h request.parameters["controller"].capitalize if request.parameters["controller"]%>#<%=h request.parameters["action"] %> -</h1> - -<p> - Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised: - <pre><code><%=h @exception.message %></code></pre> -</p> - -<p>Extracted source (around line <b>#<%=h @exception.line_number %></b>): -<pre><code><%=h @exception.source_extract %></code></pre></p> - -<p><%=h @exception.sub_template_message %></p> - -<% @real_exception = @exception - @exception = @exception.original_exception || @exception %> -<%= render :file => @rescues_path["rescues/_trace.erb"] %> -<% @exception = @real_exception %> - -<%= render :file => @rescues_path["rescues/_request_and_response.erb"] %> diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb deleted file mode 100644 index 683379da10..0000000000 --- a/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb +++ /dev/null @@ -1,2 +0,0 @@ -<h1>Unknown action</h1> -<p><%=h @exception.message %></p> diff --git a/actionpack/lib/action_controller/new_base.rb b/actionpack/lib/action_controller/new_base.rb index 7c65f1cdc1..df256985ac 100644 --- a/actionpack/lib/action_controller/new_base.rb +++ b/actionpack/lib/action_controller/new_base.rb @@ -1,7 +1,47 @@ module ActionController - autoload :AbstractBase, "action_controller/new_base/base" - autoload :HideActions, "action_controller/new_base/hide_actions" - autoload :Layouts, "action_controller/new_base/layouts" - autoload :Renderer, "action_controller/new_base/renderer" - autoload :UrlFor, "action_controller/new_base/url_for" -end
\ No newline at end of file + autoload :Base, "action_controller/new_base/base" + autoload :ConditionalGet, "action_controller/new_base/conditional_get" + autoload :HideActions, "action_controller/new_base/hide_actions" + autoload :Http, "action_controller/new_base/http" + autoload :Layouts, "action_controller/new_base/layouts" + autoload :RackConvenience, "action_controller/new_base/rack_convenience" + autoload :Rails2Compatibility, "action_controller/new_base/compatibility" + autoload :Redirector, "action_controller/new_base/redirector" + autoload :Renderer, "action_controller/new_base/renderer" + autoload :RenderOptions, "action_controller/new_base/render_options" + autoload :Renderers, "action_controller/new_base/render_options" + autoload :Rescue, "action_controller/new_base/rescuable" + autoload :Testing, "action_controller/new_base/testing" + autoload :UrlFor, "action_controller/new_base/url_for" + autoload :Session, "action_controller/new_base/session" + autoload :Helpers, "action_controller/new_base/helpers" + + # Ported modules + # require 'action_controller/routing' + autoload :Caching, 'action_controller/caching' + autoload :Dispatcher, 'action_controller/dispatch/dispatcher' + autoload :MimeResponds, 'action_controller/base/mime_responds' + autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes' + autoload :RecordIdentifier, 'action_controller/record_identifier' + autoload :Resources, 'action_controller/routing/resources' + autoload :SessionManagement, 'action_controller/base/session_management' + autoload :TestCase, 'action_controller/testing/test_case' + autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter' + autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter' + + autoload :Verification, 'action_controller/base/verification' + autoload :Flash, 'action_controller/base/chained/flash' + autoload :RequestForgeryProtection, 'action_controller/base/request_forgery_protection' + autoload :Streaming, 'action_controller/base/streaming' + autoload :HttpAuthentication, 'action_controller/base/http_authentication' + autoload :FilterParameterLogging, 'action_controller/base/filter_parameter_logging' + autoload :Translation, 'action_controller/translation' + autoload :Cookies, 'action_controller/base/cookies' + + require 'action_controller/routing' +end + +autoload :HTML, 'action_controller/vendor/html-scanner' + +require 'action_dispatch' +require 'action_view' diff --git a/actionpack/lib/action_controller/new_base/base.rb b/actionpack/lib/action_controller/new_base/base.rb index 08e7a1a0e7..d7b65d37fa 100644 --- a/actionpack/lib/action_controller/new_base/base.rb +++ b/actionpack/lib/action_controller/new_base/base.rb @@ -1,60 +1,173 @@ module ActionController - class AbstractBase < AbstractController::Base - - # :api: public - attr_internal :request, :response, :params - - # :api: public - def self.controller_name - @controller_name ||= controller_path.split("/").last - end - - # :api: public - def controller_name() self.class.controller_name end - - # :api: public - def self.controller_path - @controller_path ||= self.name.sub(/Controller$/, '').underscore - end - - # :api: public - def controller_path() self.class.controller_path end - - # :api: private - def self.action_methods - @action_names ||= Set.new(self.public_instance_methods - self::CORE_METHODS) - end - - # :api: private - def self.action_names() action_methods end - - # :api: private - def action_methods() self.class.action_names end - - # :api: private - def action_names() action_methods end - - # :api: plugin - def self.call(env) - controller = new - controller.call(env).to_rack - end - - # :api: plugin - def response_body=(body) - @_response.body = body - end - - # :api: private - def call(env) - @_request = ActionDispatch::Request.new(env) - @_response = ActionDispatch::Response.new - process(@_request.parameters[:action]) - end - - # :api: private - def to_rack - response.to_a + class Base < Http + abstract! + + include AbstractController::Benchmarker + include AbstractController::Callbacks + include AbstractController::Logger + + include ActionController::Helpers + include ActionController::HideActions + include ActionController::UrlFor + include ActionController::Redirector + include ActionController::Renderer + include ActionController::Renderers::All + include ActionController::Layouts + include ActionController::ConditionalGet + include ActionController::RackConvenience + + # Legacy modules + include SessionManagement + include ActionDispatch::StatusCodes + include ActionController::Caching + include ActionController::MimeResponds + + # Rails 2.x compatibility + include ActionController::Rails2Compatibility + + include ActionController::Cookies + include ActionController::Session + include ActionController::Flash + include ActionController::Verification + include ActionController::RequestForgeryProtection + include ActionController::Streaming + include ActionController::HttpAuthentication::Basic::ControllerMethods + include ActionController::HttpAuthentication::Digest::ControllerMethods + include ActionController::FilterParameterLogging + include ActionController::Translation + + # TODO: Extract into its own module + # This should be moved together with other normalizing behavior + module ImplicitRender + def send_action(method_name) + ret = super + default_render unless performed? + ret + end + + def default_render + render + end + + def method_for_action(action_name) + super || begin + if view_paths.find_by_parts?(action_name.to_s, {:formats => formats, :locales => [I18n.locale]}, controller_path) + "default_render" + end + end + end + end + + include ImplicitRender + + include ActionController::Rescue + + def self.inherited(klass) + ::ActionController::Base.subclasses << klass.to_s + super + end + + def self.subclasses + @subclasses ||= [] + end + + def self.app_loaded! + @subclasses.each do |subclass| + subclass.constantize._write_layout_method + end + end + + def _normalize_options(action = nil, options = {}, &blk) + if action.is_a?(Hash) + options, action = action, nil + elsif action.is_a?(String) || action.is_a?(Symbol) + key = case action = action.to_s + when %r{^/} then :file + when %r{/} then :template + else :action + end + options.merge! key => action + elsif action + options.merge! :partial => action + end + + if options.key?(:action) && options[:action].to_s.index("/") + options[:template] = options.delete(:action) + end + + if options[:status] + options[:status] = interpret_status(options[:status]).to_i + end + + options[:update] = blk if block_given? + options + end + + def render(action = nil, options = {}, &blk) + options = _normalize_options(action, options, &blk) + super(options) + end + + def render_to_string(action = nil, options = {}, &blk) + options = _normalize_options(action, options, &blk) + super(options) + end + + # Redirects the browser to the target specified in +options+. This parameter can take one of three forms: + # + # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. + # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. + # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection. + # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. + # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. + # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> + # + # Examples: + # redirect_to :action => "show", :id => 5 + # redirect_to post + # redirect_to "http://www.rubyonrails.org" + # redirect_to "/images/screenshot.jpg" + # redirect_to articles_url + # redirect_to :back + # + # The redirection happens as a "302 Moved" header unless otherwise specified. + # + # Examples: + # redirect_to post_url(@post), :status=>:found + # redirect_to :action=>'atom', :status=>:moved_permanently + # redirect_to post_url(@post), :status=>301 + # redirect_to :action=>'atom', :status=>302 + # + # When using <tt>redirect_to :back</tt>, if there is no referrer, + # RedirectBackError will be raised. You may specify some fallback + # behavior for this case by rescuing RedirectBackError. + def redirect_to(options = {}, response_status = {}) #:doc: + raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? + + status = if options.is_a?(Hash) && options.key?(:status) + interpret_status(options.delete(:status)) + elsif response_status.key?(:status) + interpret_status(response_status[:status]) + else + 302 + end + + url = case options + # The scheme name consist of a letter followed by any combination of + # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") + # characters; and is terminated by a colon (":"). + when %r{^\w[\w\d+.-]*:.*} + options + when String + request.protocol + request.host_with_port + options + when :back + raise RedirectBackError unless refer = request.headers["Referer"] + refer + else + url_for(options) + end + + super(url, status) end end end diff --git a/actionpack/lib/action_controller/new_base/compatibility.rb b/actionpack/lib/action_controller/new_base/compatibility.rb new file mode 100644 index 0000000000..f278c2da14 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/compatibility.rb @@ -0,0 +1,138 @@ +module ActionController + module Rails2Compatibility + extend ActiveSupport::Concern + + class ::ActionController::ActionControllerError < StandardError #:nodoc: + end + + # Temporary hax + included do + ::ActionController::UnknownAction = ::AbstractController::ActionNotFound + ::ActionController::DoubleRenderError = ::AbstractController::DoubleRenderError + + cattr_accessor :session_options + self.session_options = {} + + cattr_accessor :allow_concurrency + self.allow_concurrency = false + + cattr_accessor :param_parsers + self.param_parsers = { Mime::MULTIPART_FORM => :multipart_form, + Mime::URL_ENCODED_FORM => :url_encoded_form, + Mime::XML => :xml_simple, + Mime::JSON => :json } + + cattr_accessor :relative_url_root + self.relative_url_root = ENV['RAILS_RELATIVE_URL_ROOT'] + + cattr_accessor :default_charset + self.default_charset = "utf-8" + + # cattr_reader :protected_instance_variables + cattr_accessor :protected_instance_variables + self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller + @action_name @before_filter_chain_aborted @action_cache_path @_headers @_params + @_flash @_response) + + # Indicates whether or not optimise the generated named + # route helper methods + cattr_accessor :optimise_named_routes + self.optimise_named_routes = true + + cattr_accessor :resources_path_names + self.resources_path_names = { :new => 'new', :edit => 'edit' } + + # Controls the resource action separator + cattr_accessor :resource_action_separator + self.resource_action_separator = "/" + + cattr_accessor :use_accept_header + self.use_accept_header = true + + cattr_accessor :page_cache_directory + self.page_cache_directory = defined?(Rails.public_path) ? Rails.public_path : "" + + cattr_reader :cache_store + + cattr_accessor :consider_all_requests_local + self.consider_all_requests_local = true + + # Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets, + # and images to a dedicated asset server away from the main web server. Example: + # ActionController::Base.asset_host = "http://assets.example.com" + cattr_accessor :asset_host + + cattr_accessor :ip_spoofing_check + self.ip_spoofing_check = true + end + + # For old tests + def initialize_template_class(*) end + def assign_shortcuts(*) end + + # TODO: Remove this after we flip + def template + @template ||= _action_view + end + + def process_action(*) + template + super + end + + module ClassMethods + def consider_all_requests_local + end + + def rescue_action(env) + raise env["action_dispatch.rescue.exception"] + end + + # Defines the storage option for cached fragments + def cache_store=(store_option) + @@cache_store = ActiveSupport::Cache.lookup_store(store_option) + end + end + + def render_to_body(options) + if options.is_a?(Hash) && options.key?(:template) + options[:template].sub!(/^\//, '') + end + + options[:text] = nil if options[:nothing] == true + + body = super + body = [' '] if body.blank? + body + end + + def _handle_method_missing + method_missing(@_action_name.to_sym) + end + + def method_for_action(action_name) + super || (respond_to?(:method_missing) && "_handle_method_missing") + end + + def _layout_prefix(name) + super unless name =~ /\blayouts/ + end + + def performed? + response_body + end + + # ==== Request only view path switching ==== + def append_view_path(path) + view_paths.push(*path) + end + + def prepend_view_path(path) + view_paths.unshift(*path) + end + + def view_paths + _action_view.view_paths + end + end +end diff --git a/actionpack/lib/action_controller/new_base/conditional_get.rb b/actionpack/lib/action_controller/new_base/conditional_get.rb new file mode 100644 index 0000000000..8bd6db500b --- /dev/null +++ b/actionpack/lib/action_controller/new_base/conditional_get.rb @@ -0,0 +1,133 @@ +module ActionController + module ConditionalGet + extend ActiveSupport::Concern + + depends_on RackConvenience + + # Sets the etag, last_modified, or both on the response and renders a + # "304 Not Modified" response if the request is already fresh. + # + # Parameters: + # * <tt>:etag</tt> + # * <tt>:last_modified</tt> + # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches). + # + # Example: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true) + # end + # + # This will render the show template if the request isn't sending a matching etag or + # If-Modified-Since header and just a "304 Not Modified" response if there's a match. + # + def fresh_when(options) + options.assert_valid_keys(:etag, :last_modified, :public) + + response.etag = options[:etag] if options[:etag] + response.last_modified = options[:last_modified] if options[:last_modified] + + if options[:public] + cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip } + cache_control.delete("private") + cache_control.delete("no-cache") + cache_control << "public" + response.headers["Cache-Control"] = cache_control.join(', ') + end + + if request.fresh?(response) + head :not_modified + end + end + + # Return a response that has no content (merely headers). The options + # argument is interpreted to be a hash of header names and values. + # This allows you to easily return a response that consists only of + # significant headers: + # + # head :created, :location => person_path(@person) + # + # It can also be used to return exceptional conditions: + # + # return head(:method_not_allowed) unless request.post? + # return head(:bad_request) unless valid_request? + # render + def head(*args) + if args.length > 2 + raise ArgumentError, "too many arguments to head" + elsif args.empty? + raise ArgumentError, "too few arguments to head" + end + options = args.extract_options! + status = args.shift || options.delete(:status) || :ok + + options.each do |key, value| + headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s + end + + render :nothing => true, :status => status + end + + # Sets the etag and/or last_modified on the response and checks it against + # the client request. If the request doesn't match the options provided, the + # request is considered stale and should be generated from scratch. Otherwise, + # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent. + # + # Parameters: + # * <tt>:etag</tt> + # * <tt>:last_modified</tt> + # * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches). + # + # Example: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(:etag => @article, :last_modified => @article.created_at.utc) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + def stale?(options) + fresh_when(options) + !request.fresh?(response) + end + + # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that + # intermediate caches shouldn't cache the response. + # + # Examples: + # expires_in 20.minutes + # expires_in 3.hours, :public => true + # expires in 3.hours, 'max-stale' => 5.hours, :public => true + # + # This method will overwrite an existing Cache-Control header. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. + def expires_in(seconds, options = {}) #:doc: + cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip } + + cache_control << "max-age=#{seconds}" + cache_control.delete("no-cache") + if options[:public] + cache_control.delete("private") + cache_control << "public" + else + cache_control << "private" + end + + # This allows for additional headers to be passed through like 'max-stale' => 5.hours + cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} + + response.headers["Cache-Control"] = cache_control.join(', ') + end + + # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or + # intermediate caches (like caching proxy servers). + def expires_now #:doc: + response.headers["Cache-Control"] = "no-cache" + end + end +end diff --git a/actionpack/lib/action_controller/new_base/helpers.rb b/actionpack/lib/action_controller/new_base/helpers.rb new file mode 100644 index 0000000000..c2ebd343e3 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/helpers.rb @@ -0,0 +1,129 @@ +require 'active_support/core_ext/load_error' +require 'active_support/core_ext/name_error' +require 'active_support/dependencies' + +module ActionController + module Helpers + extend ActiveSupport::Concern + + depends_on AbstractController::Helpers + + included do + # Set the default directory for helpers + class_inheritable_accessor :helpers_dir + self.helpers_dir = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers") + end + + module ClassMethods + def inherited(klass) + klass.__send__ :default_helper_module! + super + end + + # The +helper+ class method can take a series of helper module names, a block, or both. + # + # * <tt>*args</tt>: One or more modules, strings or symbols, or the special symbol <tt>:all</tt>. + # * <tt>&block</tt>: A block defining helper methods. + # + # ==== Examples + # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file + # and include the module in the template class. The second form illustrates how to include custom helpers + # when working with namespaced controllers, or other cases where the file containing the helper definition is not + # in one of Rails' standard load paths: + # helper :foo # => requires 'foo_helper' and includes FooHelper + # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper + # + # When the argument is a module it will be included directly in the template class. + # helper FooHelper # => includes FooHelper + # + # When the argument is the symbol <tt>:all</tt>, the controller will include all helpers beneath + # <tt>ActionController::Base.helpers_dir</tt> (defaults to <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT). + # helper :all + # + # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available + # to the template. + # # One line + # helper { def hello() "Hello, world!" end } + # # Multi-line + # helper do + # def foo(bar) + # "#{bar} is the very best" + # end + # end + # + # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of + # +symbols+, +strings+, +modules+ and blocks. + # helper(:three, BlindHelper) { def mice() 'mice' end } + # + def helper(*args, &block) + args.flatten.each do |arg| + case arg + when :all + helper all_application_helpers + when String, Symbol + file_name = arg.to_s.underscore + '_helper' + class_name = file_name.camelize + + begin + require_dependency(file_name) + rescue LoadError => load_error + requiree = / -- (.*?)(\.rb)?$/.match(load_error.message).to_a[1] + if requiree == file_name + msg = "Missing helper file helpers/#{file_name}.rb" + raise LoadError.new(msg).copy_blame!(load_error) + else + raise + end + end + + super class_name.constantize + else + super args + end + end + + # Evaluate block in template class if given. + master_helper_module.module_eval(&block) if block_given? + end + + # Declares helper accessors for controller attributes. For example, the + # following adds new +name+ and <tt>name=</tt> instance methods to a + # controller and makes them available to the view: + # helper_attr :name + # attr_accessor :name + def helper_attr(*attrs) + attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } + end + + # Provides a proxy to access helpers methods from outside the view. + def helpers + unless @helper_proxy + @helper_proxy = ActionView::Base.new + @helper_proxy.extend master_helper_module + else + @helper_proxy + end + end + + private + def default_helper_module! + unless name.blank? + module_name = name.sub(/Controller$|$/, 'Helper') + module_path = module_name.split('::').map { |m| m.underscore }.join('/') + require_dependency module_path + helper module_name.constantize + end + rescue MissingSourceFile => e + raise unless e.is_missing? module_path + rescue NameError => e + raise unless e.missing_name? module_name + end + + # Extract helper names from files in app/helpers/**/*.rb + def all_application_helpers + extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/ + Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' } + end + end + end +end diff --git a/actionpack/lib/action_controller/new_base/hide_actions.rb b/actionpack/lib/action_controller/new_base/hide_actions.rb index 473a8ea72b..b45e520bee 100644 --- a/actionpack/lib/action_controller/new_base/hide_actions.rb +++ b/actionpack/lib/action_controller/new_base/hide_actions.rb @@ -1,31 +1,39 @@ module ActionController module HideActions - setup do + extend ActiveSupport::Concern + + included do extlib_inheritable_accessor :hidden_actions - self.hidden_actions ||= Set.new + self.hidden_actions ||= Set.new end - - def action_methods() self.class.action_names end - def action_names() action_methods end - - private - - def respond_to_action?(action_name) - !hidden_actions.include?(action_name) && (super || respond_to?(:method_missing)) + + def action_methods + self.class.action_names end - - module ClassMethods - def hide_action(*args) - args.each do |arg| - self.hidden_actions << arg.to_s - end - end - - def action_methods - @action_names ||= Set.new(super.reject {|name| self.hidden_actions.include?(name.to_s)}) - end - def self.action_names() action_methods end + def action_names + action_methods end + + private + def action_method?(action_name) + !hidden_actions.include?(action_name) && super + end + + module ClassMethods + def hide_action(*args) + args.each do |arg| + self.hidden_actions << arg.to_s + end + end + + def action_methods + @action_names ||= Set.new(super.reject {|name| self.hidden_actions.include?(name.to_s)}) + end + + def self.action_names + action_methods + end + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/new_base/http.rb b/actionpack/lib/action_controller/new_base/http.rb new file mode 100644 index 0000000000..c96aaaa865 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/http.rb @@ -0,0 +1,91 @@ +require 'action_controller/abstract' +require 'active_support/core_ext/module/delegation' + +module ActionController + class Http < AbstractController::Base + abstract! + + # :api: public + attr_internal :params, :env + + # :api: public + def self.controller_name + @controller_name ||= controller_path.split("/").last + end + + # :api: public + def controller_name + self.class.controller_name + end + + # :api: public + def self.controller_path + @controller_path ||= self.name.sub(/Controller$/, '').underscore + end + + # :api: public + def controller_path + self.class.controller_path + end + + # :api: private + def self.action_names + action_methods + end + + # :api: private + def action_names + action_methods + end + + # :api: plugin + def self.call(env) + controller = new + controller.call(env).to_rack + end + + # The details below can be overridden to support a specific + # Request and Response object. The default ActionController::Base + # implementation includes RackConvenience, which makes a request + # and response object available. You might wish to control the + # environment and response manually for performance reasons. + + attr_internal :status, :headers, :content_type + + def initialize(*) + @_headers = {} + super + end + + # Basic implements for content_type=, location=, and headers are + # provided to reduce the dependency on the RackConvenience module + # in Renderer and Redirector. + + def content_type=(type) + headers["Content-Type"] = type.to_s + end + + def location=(url) + headers["Location"] = url + end + + # :api: private + def call(name, env) + @_env = env + process(name) + to_rack + end + + # :api: private + def to_rack + [status, headers, response_body] + end + + def self.action(name) + @actions ||= {} + @actions[name.to_s] ||= proc do |env| + new.call(name, env) + end + end + end +end diff --git a/actionpack/lib/action_controller/new_base/layouts.rb b/actionpack/lib/action_controller/new_base/layouts.rb index a8e0809ac6..727358c394 100644 --- a/actionpack/lib/action_controller/new_base/layouts.rb +++ b/actionpack/lib/action_controller/new_base/layouts.rb @@ -1,37 +1,34 @@ module ActionController module Layouts + extend ActiveSupport::Concern + depends_on ActionController::Renderer depends_on AbstractController::Layouts - + module ClassMethods def _implied_layout_name controller_path end end - - def render_to_body(options) - # render :text => ..., :layout => ... - # or - # render :anything_else - if !options.key?(:text) || options.key?(:layout) - options[:_layout] = options.key?(:layout) ? _layout_for_option(options[:layout]) : _default_layout + + private + def _determine_template(options) + super + if (!options.key?(:text) && !options.key?(:inline) && !options.key?(:partial)) || options.key?(:layout) + options[:_layout] = _layout_for_option(options.key?(:layout) ? options[:layout] : :none, options[:_template].details) + end end - - super - end - - private - - def _layout_for_option(name) - case name - when String then _layout_for_name(name) - when true then _default_layout(true) - when false, nil then nil - else - raise ArgumentError, - "String, true, or false, expected for `layout'; you passed #{name.inspect}" + + def _layout_for_option(name, details) + case name + when String then _layout_for_name(name, details) + when true then _default_layout(true, details) + when :none then _default_layout(false, details) + when false, nil then nil + else + raise ArgumentError, + "String, true, or false, expected for `layout'; you passed #{name.inspect}" + end end - end - end end diff --git a/actionpack/lib/action_controller/new_base/rack_convenience.rb b/actionpack/lib/action_controller/new_base/rack_convenience.rb new file mode 100644 index 0000000000..5dfa7d12f3 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/rack_convenience.rb @@ -0,0 +1,33 @@ +module ActionController + module RackConvenience + extend ActiveSupport::Concern + + included do + delegate :headers, :status=, :location=, + :status, :location, :content_type, :to => "@_response" + attr_internal :request, :response + end + + def call(name, env) + @_request = ActionDispatch::Request.new(env) + @_response = ActionDispatch::Response.new + @_response.request = request + super + end + + def params + @_params ||= @_request.parameters + end + + # :api: private + def to_rack + @_response.prepare! + @_response.to_a + end + + def response_body=(body) + response.body = body if response + super + end + end +end diff --git a/actionpack/lib/action_controller/new_base/redirector.rb b/actionpack/lib/action_controller/new_base/redirector.rb new file mode 100644 index 0000000000..20060b001f --- /dev/null +++ b/actionpack/lib/action_controller/new_base/redirector.rb @@ -0,0 +1,19 @@ +module ActionController + class RedirectBackError < AbstractController::Error #:nodoc: + DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].' + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + + module Redirector + def redirect_to(url, status) #:doc: + raise AbstractController::DoubleRenderError if response_body + logger.info("Redirected to #{url}") if logger && logger.info? + self.status = status + self.location = url.gsub(/[\r\n]/, '') + self.response_body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>" + end + end +end diff --git a/actionpack/lib/action_controller/new_base/render_options.rb b/actionpack/lib/action_controller/new_base/render_options.rb new file mode 100644 index 0000000000..04b954134f --- /dev/null +++ b/actionpack/lib/action_controller/new_base/render_options.rb @@ -0,0 +1,103 @@ +module ActionController + module RenderOptions + extend ActiveSupport::Concern + + included do + extlib_inheritable_accessor :_renderers + self._renderers = [] + end + + module ClassMethods + def _write_render_options + renderers = _renderers.map do |r| + <<-RUBY_EVAL + if options.key?(:#{r}) + _process_options(options) + return _render_#{r}(options[:#{r}], options) + end + RUBY_EVAL + end + + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def _handle_render_options(options) + #{renderers.join} + end + RUBY_EVAL + end + + def _add_render_option(name) + _renderers << name + _write_render_options + end + end + + def render_to_body(options) + _handle_render_options(options) || super + end + end + + module RenderOption #:nodoc: + def self.extended(base) + base.extend ActiveSupport::Concern + base.depends_on ::ActionController::RenderOptions + + def base.register_renderer(name) + included { _add_render_option(name) } + end + end + end + + module Renderers + module Json + extend RenderOption + register_renderer :json + + def _render_json(json, options) + json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str) + json = "#{options[:callback]}(#{json})" unless options[:callback].blank? + self.content_type ||= Mime::JSON + self.response_body = json + end + end + + module Js + extend RenderOption + register_renderer :js + + def _render_js(js, options) + self.content_type ||= Mime::JS + self.response_body = js + end + end + + module Xml + extend RenderOption + register_renderer :xml + + def _render_xml(xml, options) + self.content_type ||= Mime::XML + self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml : xml + end + end + + module RJS + extend RenderOption + register_renderer :update + + def _render_update(proc, options) + generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(_action_view, &proc) + self.content_type = Mime::JS + self.response_body = generator.to_s + end + end + + module All + extend ActiveSupport::Concern + + depends_on ActionController::Renderers::Json + depends_on ActionController::Renderers::Js + depends_on ActionController::Renderers::Xml + depends_on ActionController::Renderers::RJS + end + end +end diff --git a/actionpack/lib/action_controller/new_base/renderer.rb b/actionpack/lib/action_controller/new_base/renderer.rb index ed34c46aed..e132d4fdbb 100644 --- a/actionpack/lib/action_controller/new_base/renderer.rb +++ b/actionpack/lib/action_controller/new_base/renderer.rb @@ -1,62 +1,78 @@ module ActionController module Renderer + extend ActiveSupport::Concern + depends_on AbstractController::Renderer - - def initialize(*) - self.formats = [:html] + + def process_action(*) + self.formats = request.formats.map {|x| x.to_sym} super end - - def render(action, options = {}) - # TODO: Move this into #render_to_body - if action.is_a?(Hash) - options, action = action, nil - else - options.merge! :action => action + + def render(options) + super + options[:_template] ||= _action_view._partial + self.content_type ||= begin + mime = options[:_template].mime_type + formats.include?(mime && mime.to_sym) || formats.include?(:all) ? mime : Mime::Type.lookup_by_extension(formats.first) end - - _process_options(options) - - self.response_body = render_to_body(options) + response_body end def render_to_body(options) - unless options.is_a?(Hash) - options = {:action => options} - end + _process_options(options) - if options.key?(:text) - options[:_template] = ActionView::TextTemplate.new(_text(options)) - template = nil - elsif options.key?(:template) - options[:_template_name] = options[:template] - elsif options.key?(:action) - options[:_template_name] = options[:action].to_s - options[:_prefix] = _prefix + if options.key?(:partial) + _render_partial(options[:partial], options) end - - super(options) + + super end - - private - - def _prefix - controller_path - end - - def _text(options) - text = options[:text] - - case text - when nil then " " - else text.to_s + + private + def _prefix + controller_path end - end - - def _process_options(options) - if status = options[:status] - response.status = status.to_i + + def _determine_template(options) + if options.key?(:text) + options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) + elsif options.key?(:inline) + handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") + template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) + options[:_template] = template + elsif options.key?(:template) + options[:_template_name] = options[:template] + elsif options.key?(:file) + options[:_template_name] = options[:file] + elsif !options.key?(:partial) + options[:_template_name] = (options[:action] || action_name).to_s + options[:_prefix] = _prefix + end + + super + end + + def _render_partial(partial, options) + case partial + when true + options[:_prefix] = _prefix + when String + options[:_prefix] = _prefix unless partial.index('/') + options[:_template_name] = partial + else + options[:_partial_object] = true + return + end + + options[:_partial] = options[:object] || true + end + + def _process_options(options) + status, content_type, location = options.values_at(:status, :content_type, :location) + self.status = status if status + self.content_type = content_type if content_type + self.headers["Location"] = url_for(location) if location end - end end end diff --git a/actionpack/lib/action_controller/new_base/rescuable.rb b/actionpack/lib/action_controller/new_base/rescuable.rb new file mode 100644 index 0000000000..029e643d93 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/rescuable.rb @@ -0,0 +1,52 @@ +module ActionController #:nodoc: + # Actions that fail to perform as expected throw exceptions. These + # exceptions can either be rescued for the public view (with a nice + # user-friendly explanation) or for the developers view (with tons of + # debugging information). The developers view is already implemented by + # the Action Controller, but the public view should be tailored to your + # specific application. + # + # The default behavior for public exceptions is to render a static html + # file with the name of the error code thrown. If no such file exists, an + # empty response is sent with the correct status code. + # + # You can override what constitutes a local request by overriding the + # <tt>local_request?</tt> method in your own controller. Custom rescue + # behavior is achieved by overriding the <tt>rescue_action_in_public</tt> + # and <tt>rescue_action_locally</tt> methods. + module Rescue + extend ActiveSupport::Concern + + included do + include ActiveSupport::Rescuable + end + + module ClassMethods + # This can be removed once we can move action(:_rescue_action) into middlewares.rb + # Currently, it does controller.method(:rescue_action), which is hiding the implementation + # difference between the old and new base. + def rescue_action(env) + action(:_rescue_action).call(env) + end + end + + attr_internal :rescued_exception + + private + def method_for_action(action_name) + return action_name if self.rescued_exception = request.env.delete("action_dispatch.rescue.exception") + super + end + + def _rescue_action + rescue_with_handler(rescued_exception) || raise(rescued_exception) + end + + def process_action(*) + super + rescue Exception => exception + self.rescued_exception = exception + _rescue_action + end + end +end diff --git a/actionpack/lib/action_controller/new_base/session.rb b/actionpack/lib/action_controller/new_base/session.rb new file mode 100644 index 0000000000..a585630230 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/session.rb @@ -0,0 +1,15 @@ +module ActionController + module Session + extend ActiveSupport::Concern + + depends_on RackConvenience + + def session + @_request.session + end + + def reset_session + @_request.reset_session + end + end +end diff --git a/actionpack/lib/action_controller/new_base/testing.rb b/actionpack/lib/action_controller/new_base/testing.rb new file mode 100644 index 0000000000..e8d210d149 --- /dev/null +++ b/actionpack/lib/action_controller/new_base/testing.rb @@ -0,0 +1,39 @@ +module ActionController + module Testing + extend ActiveSupport::Concern + + depends_on RackConvenience + + # OMG MEGA HAX + def process_with_new_base_test(request, response) + @_request = request + @_response = response + @_response.request = request + ret = process(request.parameters[:action]) + @_response.body ||= self.response_body + @_response.prepare! + set_test_assigns + ret + end + + def set_test_assigns + @assigns = {} + (instance_variable_names - self.class.protected_instance_variables).each do |var| + name, value = var[1..-1], instance_variable_get(var) + @assigns[name] = value + end + end + + # TODO : Rewrite tests using controller.headers= to use Rack env + def headers=(new_headers) + @_response ||= ActionDispatch::Response.new + @_response.headers.replace(new_headers) + end + + module ClassMethods + def before_filters + _process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name} + end + end + end +end diff --git a/actionpack/lib/action_controller/new_base/url_for.rb b/actionpack/lib/action_controller/new_base/url_for.rb index af5b21012b..902cac4d15 100644 --- a/actionpack/lib/action_controller/new_base/url_for.rb +++ b/actionpack/lib/action_controller/new_base/url_for.rb @@ -1,5 +1,14 @@ module ActionController module UrlFor + extend ActiveSupport::Concern + + depends_on RackConvenience + + def process_action(*) + initialize_current_url + super + end + def initialize_current_url @url = UrlRewriter.new(request, params.clone) end @@ -16,7 +25,7 @@ module ActionController # by this method. def default_url_options(options = nil) end - + def rewrite_options(options) #:nodoc: if defaults = default_url_options(options) defaults.merge(options) @@ -24,7 +33,7 @@ module ActionController options end end - + def url_for(options = {}) options ||= {} case options @@ -37,4 +46,4 @@ module ActionController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 6bda27e23a..b4408e4e1d 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module' + module ActionController # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or # Active Resources or pretty much any other model type that has an id. These patterns are then used to try elevate diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_controller/routing.rb index c0eb61340b..ce59866531 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_controller/routing.rb @@ -1,5 +1,9 @@ require 'cgi' require 'uri' +require 'set' + +require 'active_support/core_ext/module/aliasing' +require 'active_support/core_ext/module/attribute_accessors' require 'action_controller/routing/optimisations' require 'action_controller/routing/routing_ext' require 'action_controller/routing/route' diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb index d9590c88b8..42ad12e1ea 100644 --- a/actionpack/lib/action_controller/routing/builder.rb +++ b/actionpack/lib/action_controller/routing/builder.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/except' + module ActionController module Routing class RouteBuilder #:nodoc: diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb index e2077edad8..eba05a3c5a 100644 --- a/actionpack/lib/action_controller/routing/route.rb +++ b/actionpack/lib/action_controller/routing/route.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/misc' + module ActionController module Routing class Route #:nodoc: @@ -65,7 +67,7 @@ module ActionController # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ # def parameter_shell - @parameter_shell ||= returning({}) do |shell| + @parameter_shell ||= {}.tap do |shell| requirements.each do |key, requirement| shell[key] = requirement unless requirement.is_a? Regexp end @@ -76,7 +78,7 @@ module ActionController # includes keys that appear inside the path, and keys that have requirements # placed upon them. def significant_keys - @significant_keys ||= returning([]) do |sk| + @significant_keys ||= [].tap do |sk| segments.each { |segment| sk << segment.key if segment.respond_to? :key } sk.concat requirements.keys sk.uniq! @@ -86,7 +88,7 @@ module ActionController # Return a hash of key/value pairs representing the keys in the route that # have defaults, or which are specified by non-regexp requirements. def defaults - @defaults ||= returning({}) do |hash| + @defaults ||= {}.tap do |hash| segments.each do |segment| next unless segment.respond_to? :default hash[segment.key] = segment.default unless segment.default.nil? diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb index 70cd1f642d..45ad8a3a3b 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_controller/routing/route_set.rb @@ -430,7 +430,7 @@ module ActionController def call(env) request = ActionDispatch::Request.new(env) app = Routing::Routes.recognize(request) - app.call(env).to_a + app.action(request.parameters[:action] || 'index').call(env) end def recognize(request) diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb index 4f936d51d2..2603855476 100644 --- a/actionpack/lib/action_controller/routing/segments.rb +++ b/actionpack/lib/action_controller/routing/segments.rb @@ -1,7 +1,7 @@ module ActionController module Routing class Segment #:nodoc: - RESERVED_PCHAR = ':@&=+$,;' + RESERVED_PCHAR = ':@&=+$,;%' SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" if RUBY_VERSION >= '1.9' UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze diff --git a/actionpack/lib/action_controller/testing/assertions/dom.rb b/actionpack/lib/action_controller/testing/assertions/dom.rb deleted file mode 100644 index 5ffe5f1883..0000000000 --- a/actionpack/lib/action_controller/testing/assertions/dom.rb +++ /dev/null @@ -1,39 +0,0 @@ -module ActionController - module Assertions - module DomAssertions - # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) - # - # ==== Examples - # - # # assert that the referenced method generates the appropriate HTML string - # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com") - # - def assert_dom_equal(expected, actual, message = "") - clean_backtrace do - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s) - - assert_block(full_message) { expected_dom == actual_dom } - end - end - - # The negated form of +assert_dom_equivalent+. - # - # ==== Examples - # - # # assert that the referenced method does not generate the specified HTML string - # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com") - # - def assert_dom_not_equal(expected, actual, message = "") - clean_backtrace do - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s) - - assert_block(full_message) { expected_dom != actual_dom } - end - end - end - end -end diff --git a/actionpack/lib/action_controller/testing/assertions/model.rb b/actionpack/lib/action_controller/testing/assertions/model.rb deleted file mode 100644 index 3a7b39b106..0000000000 --- a/actionpack/lib/action_controller/testing/assertions/model.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ActionController - module Assertions - module ModelAssertions - # Ensures that the passed record is valid by Active Record standards and - # returns any error messages if it is not. - # - # ==== Examples - # - # # assert that a newly created record is valid - # model = Model.new - # assert_valid(model) - # - def assert_valid(record) - ::ActiveSupport::Deprecation.warn("assert_valid is deprecated. Use assert record.valid? instead", caller) - clean_backtrace do - assert record.valid?, record.errors.full_messages.join("\n") - end - end - end - end -end diff --git a/actionpack/lib/action_controller/testing/assertions/response.rb b/actionpack/lib/action_controller/testing/assertions/response.rb deleted file mode 100644 index ca0a9bbf52..0000000000 --- a/actionpack/lib/action_controller/testing/assertions/response.rb +++ /dev/null @@ -1,150 +0,0 @@ -module ActionController - module Assertions - # A small suite of assertions that test responses from Rails applications. - module ResponseAssertions - # Asserts that the response is one of the following types: - # - # * <tt>:success</tt> - Status code was 200 - # * <tt>:redirect</tt> - Status code was in the 300-399 range - # * <tt>:missing</tt> - Status code was 404 - # * <tt>:error</tt> - Status code was in the 500-599 range - # - # You can also pass an explicit status number like assert_response(501) - # or its symbolic equivalent assert_response(:not_implemented). - # See ActionDispatch::StatusCodes for a full list. - # - # ==== Examples - # - # # assert that the response was a redirection - # assert_response :redirect - # - # # assert that the response code was status code 401 (unauthorized) - # assert_response 401 - # - def assert_response(type, message = nil) - clean_backtrace do - if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?") - assert_block("") { true } # to count the assertion - elsif type.is_a?(Fixnum) && @response.response_code == type - assert_block("") { true } # to count the assertion - elsif type.is_a?(Symbol) && @response.response_code == ActionDispatch::StatusCodes::SYMBOL_TO_STATUS_CODE[type] - assert_block("") { true } # to count the assertion - else - if @response.error? - exception = @response.template.instance_variable_get(:@exception) - exception_message = exception && exception.message - assert_block(build_message(message, "Expected response to be a <?>, but was <?>\n<?>", type, @response.response_code, exception_message.to_s)) { false } - else - assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false } - end - end - end - end - - # Assert that the redirection options passed in match those of the redirect called in the latest action. - # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also - # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on. - # - # ==== Examples - # - # # assert that the redirection was to the "index" action on the WeblogController - # assert_redirected_to :controller => "weblog", :action => "index" - # - # # assert that the redirection was to the named route login_url - # assert_redirected_to login_url - # - # # assert that the redirection was to the url for @customer - # assert_redirected_to @customer - # - def assert_redirected_to(options = {}, message=nil) - clean_backtrace do - assert_response(:redirect, message) - return true if options == @response.redirected_to - - # Support partial arguments for hash redirections - if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash) - return true if options.all? {|(key, value)| @response.redirected_to[key] == value} - end - - redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to) - options_after_normalisation = normalize_argument_to_redirection(options) - - if redirected_to_after_normalisation != options_after_normalisation - flunk "Expected response to be a redirect to <#{options_after_normalisation}> but was a redirect to <#{redirected_to_after_normalisation}>" - end - end - end - - # Asserts that the request was rendered with the appropriate template file or partials - # - # ==== Examples - # - # # assert that the "new" view template was rendered - # assert_template "new" - # - # # assert that the "_customer" partial was rendered twice - # assert_template :partial => '_customer', :count => 2 - # - # # assert that no partials were rendered - # assert_template :partial => false - # - def assert_template(options = {}, message = nil) - clean_backtrace do - case options - when NilClass, String - rendered = @response.rendered[:template].to_s - msg = build_message(message, - "expecting <?> but rendering with <?>", - options, rendered) - assert_block(msg) do - if options.nil? - @response.rendered[:template].blank? - else - rendered.to_s.match(options) - end - end - when Hash - if expected_partial = options[:partial] - partials = @response.rendered[:partials] - if expected_count = options[:count] - found = partials.detect { |p, _| p.to_s.match(expected_partial) } - actual_count = found.nil? ? 0 : found.second - msg = build_message(message, - "expecting ? to be rendered ? time(s) but rendered ? time(s)", - expected_partial, expected_count, actual_count) - assert(actual_count == expected_count.to_i, msg) - else - msg = build_message(message, - "expecting partial <?> but action rendered <?>", - options[:partial], partials.keys) - assert(partials.keys.any? { |p| p.to_s.match(expected_partial) }, msg) - end - else - assert @response.rendered[:partials].empty?, - "Expected no partials to be rendered" - end - end - end - end - - private - # Proxy to to_param if the object will respond to it. - def parameterize(value) - value.respond_to?(:to_param) ? value.to_param : value - end - - def normalize_argument_to_redirection(fragment) - after_routing = @controller.url_for(fragment) - if after_routing =~ %r{^\w+://.*} - after_routing - else - # FIXME - this should probably get removed. - if after_routing.first != '/' - after_routing = '/' + after_routing - end - @request.protocol + @request.host_with_port + after_routing - end - end - end - end -end diff --git a/actionpack/lib/action_controller/testing/assertions/routing.rb b/actionpack/lib/action_controller/testing/assertions/routing.rb deleted file mode 100644 index 5101751cea..0000000000 --- a/actionpack/lib/action_controller/testing/assertions/routing.rb +++ /dev/null @@ -1,146 +0,0 @@ -module ActionController - module Assertions - # Suite of assertions to test routes generated by Rails and the handling of requests made to them. - module RoutingAssertions - # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) - # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+. - # - # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes - # requiring a specific HTTP method. The hash should contain a :path with the incoming request path - # and a :method containing the required HTTP verb. - # - # # assert that POSTing to /items will call the create action on ItemsController - # assert_recognizes {:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post} - # - # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used - # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the - # extras argument, appending the query string on the path directly will not work. For example: - # - # # assert that a path of '/items/list/1?view=print' returns the correct options - # assert_recognizes {:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" } - # - # The +message+ parameter allows you to pass in an error message that is displayed upon failure. - # - # ==== Examples - # # Check the default route (i.e., the index action) - # assert_recognizes {:controller => 'items', :action => 'index'}, 'items' - # - # # Test a specific action - # assert_recognizes {:controller => 'items', :action => 'list'}, 'items/list' - # - # # Test an action with a parameter - # assert_recognizes {:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1' - # - # # Test a custom route - # assert_recognizes {:controller => 'items', :action => 'show', :id => '1'}, 'view/item1' - # - # # Check a Simply RESTful generated route - # assert_recognizes list_items_url, 'items/list' - def assert_recognizes(expected_options, path, extras={}, message=nil) - if path.is_a? Hash - request_method = path[:method] - path = path[:path] - else - request_method = nil - end - - clean_backtrace do - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - request = recognized_request_for(path, request_method) - - expected_options = expected_options.clone - extras.each_key { |key| expected_options.delete key } unless extras.nil? - - expected_options.stringify_keys! - routing_diff = expected_options.diff(request.path_parameters) - msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>", - request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) - assert_block(msg) { request.path_parameters == expected_options } - end - end - - # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. - # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in - # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures. - # - # The +defaults+ parameter is unused. - # - # ==== Examples - # # Asserts that the default action is generated for a route with no action - # assert_generates "/items", :controller => "items", :action => "index" - # - # # Tests that the list action is properly routed - # assert_generates "/items/list", :controller => "items", :action => "list" - # - # # Tests the generation of a route with a parameter - # assert_generates "/items/list/1", { :controller => "items", :action => "list", :id => "1" } - # - # # Asserts that the generated route gives us our custom route - # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } - def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) - clean_backtrace do - expected_path = "/#{expected_path}" unless expected_path[0] == ?/ - # Load routes.rb if it hasn't been loaded. - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - - generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, defaults) - found_extras = options.reject {|k, v| ! extra_keys.include? k} - - msg = build_message(message, "found extras <?>, not <?>", found_extras, extras) - assert_block(msg) { found_extras == extras } - - msg = build_message(message, "The generated path <?> did not match <?>", generated_path, - expected_path) - assert_block(msg) { expected_path == generated_path } - end - end - - # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates - # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+ - # and +assert_generates+ into one step. - # - # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The - # +message+ parameter allows you to specify a custom error message to display upon failure. - # - # ==== Examples - # # Assert a basic route: a controller with the default action (index) - # assert_routing '/home', :controller => 'home', :action => 'index' - # - # # Test a route generated with a specific controller, action, and parameter (id) - # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', id => 23 - # - # # Assert a basic route (controller + default action), with an error message if it fails - # assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly' - # - # # Tests a route, providing a defaults hash - # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"} - # - # # Tests a route with a HTTP method - # assert_routing { :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" } - def assert_routing(path, options, defaults={}, extras={}, message=nil) - assert_recognizes(options, path, extras, message) - - controller, default_controller = options[:controller], defaults[:controller] - if controller && controller.include?(?/) && default_controller && default_controller.include?(?/) - options[:controller] = "/#{controller}" - end - - assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message) - end - - private - # Recognizes the route for a given path. - def recognized_request_for(path, request_method = nil) - path = "/#{path}" unless path.first == '/' - - # Assume given controller - request = ActionController::TestRequest.new - request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method - request.path = path - - ActionController::Routing::Routes.recognize(request) - request - end - end - end -end diff --git a/actionpack/lib/action_controller/testing/assertions/selector.rb b/actionpack/lib/action_controller/testing/assertions/selector.rb deleted file mode 100644 index 0d56ea5ef7..0000000000 --- a/actionpack/lib/action_controller/testing/assertions/selector.rb +++ /dev/null @@ -1,632 +0,0 @@ -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -module ActionController - module Assertions - unless const_defined?(:NO_STRIP) - NO_STRIP = %w{pre script style textarea} - end - - # Adds the +assert_select+ method for use in Rails functional - # test cases, which can be used to make assertions on the response HTML of a controller - # action. You can also call +assert_select+ within another +assert_select+ to - # make assertions on elements selected by the enclosing assertion. - # - # Use +css_select+ to select elements without making an assertions, either - # from the response HTML or elements selected by the enclosing assertion. - # - # In addition to HTML responses, you can make the following assertions: - # * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations. - # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions. - # * +assert_select_email+ - Assertions on the HTML body of an e-mail. - # - # Also see HTML::Selector to learn how to use selectors. - module SelectorAssertions - # :call-seq: - # css_select(selector) => array - # css_select(element, selector) => array - # - # Select and return all matching elements. - # - # If called with a single argument, uses that argument as a selector - # to match all elements of the current page. Returns an empty array - # if no match is found. - # - # If called with two arguments, uses the first argument as the base - # element and the second argument as the selector. Attempts to match the - # base element and any of its children. Returns an empty array if no - # match is found. - # - # The selector may be a CSS selector expression (String), an expression - # with substitution values (Array) or an HTML::Selector object. - # - # ==== Examples - # # Selects all div tags - # divs = css_select("div") - # - # # Selects all paragraph tags and does something interesting - # pars = css_select("p") - # pars.each do |par| - # # Do something fun with paragraphs here... - # end - # - # # Selects all list items in unordered lists - # items = css_select("ul>li") - # - # # Selects all form tags and then all inputs inside the form - # forms = css_select("form") - # forms.each do |form| - # inputs = css_select(form, "input") - # ... - # end - # - def css_select(*args) - # See assert_select to understand what's going on here. - arg = args.shift - - if arg.is_a?(HTML::Node) - root = arg - arg = args.shift - elsif arg == nil - raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected - matches = [] - - @selected.each do |selected| - subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) - subset.each do |match| - matches << match unless matches.any? { |m| m.equal?(match) } - end - end - - return matches - else - root = response_from_page_or_rjs - end - - case arg - when String - selector = HTML::Selector.new(arg, args) - when Array - selector = HTML::Selector.new(*arg) - when HTML::Selector - selector = arg - else raise ArgumentError, "Expecting a selector as the first argument" - end - - selector.select(root) - end - - # :call-seq: - # assert_select(selector, equality?, message?) - # assert_select(element, selector, equality?, message?) - # - # An assertion that selects elements and makes one or more equality tests. - # - # If the first argument is an element, selects all matching elements - # starting from (and including) that element and all its children in - # depth-first order. - # - # If no element if specified, calling +assert_select+ selects from the - # response HTML unless +assert_select+ is called from within an +assert_select+ block. - # - # When called with a block +assert_select+ passes an array of selected elements - # to the block. Calling +assert_select+ from the block, with no element specified, - # runs the assertion on the complete set of elements selected by the enclosing assertion. - # Alternatively the array may be iterated through so that +assert_select+ can be called - # separately for each element. - # - # - # ==== Example - # If the response contains two ordered lists, each with four list elements then: - # assert_select "ol" do |elements| - # elements.each do |element| - # assert_select element, "li", 4 - # end - # end - # - # will pass, as will: - # assert_select "ol" do - # assert_select "li", 8 - # end - # - # The selector may be a CSS selector expression (String), an expression - # with substitution values, or an HTML::Selector object. - # - # === Equality Tests - # - # The equality test may be one of the following: - # * <tt>true</tt> - Assertion is true if at least one element selected. - # * <tt>false</tt> - Assertion is true if no element selected. - # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least - # one element matches the string or regular expression. - # * <tt>Integer</tt> - Assertion is true if exactly that number of - # elements are selected. - # * <tt>Range</tt> - Assertion is true if the number of selected - # elements fit the range. - # If no equality test specified, the assertion is true if at least one - # element selected. - # - # To perform more than one equality tests, use a hash with the following keys: - # * <tt>:text</tt> - Narrow the selection to elements that have this text - # value (string or regexp). - # * <tt>:html</tt> - Narrow the selection to elements that have this HTML - # content (string or regexp). - # * <tt>:count</tt> - Assertion is true if the number of selected elements - # is equal to this value. - # * <tt>:minimum</tt> - Assertion is true if the number of selected - # elements is at least this value. - # * <tt>:maximum</tt> - Assertion is true if the number of selected - # elements is at most this value. - # - # If the method is called with a block, once all equality tests are - # evaluated the block is called with an array of all matched elements. - # - # ==== Examples - # - # # At least one form element - # assert_select "form" - # - # # Form element includes four input fields - # assert_select "form input", 4 - # - # # Page title is "Welcome" - # assert_select "title", "Welcome" - # - # # Page title is "Welcome" and there is only one title element - # assert_select "title", {:count=>1, :text=>"Welcome"}, - # "Wrong title or more than one title element" - # - # # Page contains no forms - # assert_select "form", false, "This page must contain no forms" - # - # # Test the content and style - # assert_select "body div.header ul.menu" - # - # # Use substitution values - # assert_select "ol>li#?", /item-\d+/ - # - # # All input fields in the form have a name - # assert_select "form input" do - # assert_select "[name=?]", /.+/ # Not empty - # end - def assert_select(*args, &block) - # Start with optional element followed by mandatory selector. - arg = args.shift - - if arg.is_a?(HTML::Node) - # First argument is a node (tag or text, but also HTML root), - # so we know what we're selecting from. - root = arg - arg = args.shift - elsif arg == nil - # This usually happens when passing a node/element that - # happens to be nil. - raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected - root = HTML::Node.new(nil) - root.children.concat @selected - else - # Otherwise just operate on the response document. - root = response_from_page_or_rjs - end - - # First or second argument is the selector: string and we pass - # all remaining arguments. Array and we pass the argument. Also - # accepts selector itself. - case arg - when String - selector = HTML::Selector.new(arg, args) - when Array - selector = HTML::Selector.new(*arg) - when HTML::Selector - selector = arg - else raise ArgumentError, "Expecting a selector as the first argument" - end - - # Next argument is used for equality tests. - equals = {} - case arg = args.shift - when Hash - equals = arg - when String, Regexp - equals[:text] = arg - when Integer - equals[:count] = arg - when Range - equals[:minimum] = arg.begin - equals[:maximum] = arg.end - when FalseClass - equals[:count] = 0 - when NilClass, TrueClass - equals[:minimum] = 1 - else raise ArgumentError, "I don't understand what you're trying to match" - end - - # By default we're looking for at least one match. - if equals[:count] - equals[:minimum] = equals[:maximum] = equals[:count] - else - equals[:minimum] = 1 unless equals[:minimum] - end - - # Last argument is the message we use if the assertion fails. - message = args.shift - #- message = "No match made with selector #{selector.inspect}" unless message - if args.shift - raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" - end - - matches = selector.select(root) - # If text/html, narrow down to those elements that match it. - content_mismatch = nil - if match_with = equals[:text] - matches.delete_if do |match| - text = "" - text.force_encoding(match_with.encoding) if text.respond_to?(:force_encoding) - stack = match.children.reverse - while node = stack.pop - if node.tag? - stack.concat node.children.reverse - else - content = node.content - content.force_encoding(match_with.encoding) if content.respond_to?(:force_encoding) - text << content - end - end - text.strip! unless NO_STRIP.include?(match.name) - unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) - content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text) - true - end - end - elsif match_with = equals[:html] - matches.delete_if do |match| - html = match.children.map(&:to_s).join - html.strip! unless NO_STRIP.include?(match.name) - unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) - content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html) - true - end - end - end - # Expecting foo found bar element only if found zero, not if - # found one but expecting two. - message ||= content_mismatch if matches.empty? - # Test minimum/maximum occurrence. - min, max = equals[:minimum], equals[:maximum] - message = message || %(Expected #{count_description(min, max)} matching "#{selector.to_s}", found #{matches.size}.) - assert matches.size >= min, message if min - assert matches.size <= max, message if max - - # If a block is given call that block. Set @selected to allow - # nested assert_select, which can be nested several levels deep. - if block_given? && !matches.empty? - begin - in_scope, @selected = @selected, matches - yield matches - ensure - @selected = in_scope - end - end - - # Returns all matches elements. - matches - end - - def count_description(min, max) #:nodoc: - pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')} - - if min && max && (max != min) - "between #{min} and #{max} elements" - elsif min && !(min == 1 && max == 1) - "at least #{min} #{pluralize['element', min]}" - elsif max - "at most #{max} #{pluralize['element', max]}" - end - end - - # :call-seq: - # assert_select_rjs(id?) { |elements| ... } - # assert_select_rjs(statement, id?) { |elements| ... } - # assert_select_rjs(:insert, position, id?) { |elements| ... } - # - # Selects content from the RJS response. - # - # === Narrowing down - # - # With no arguments, asserts that one or more elements are updated or - # inserted by RJS statements. - # - # Use the +id+ argument to narrow down the assertion to only statements - # that update or insert an element with that identifier. - # - # Use the first argument to narrow down assertions to only statements - # of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>, - # <tt>:show</tt>, <tt>:hide</tt>, <tt>:toggle</tt>, <tt>:remove</tt> and - # <tt>:insert_html</tt>. - # - # Use the argument <tt>:insert</tt> followed by an insertion position to narrow - # down the assertion to only statements that insert elements in that - # position. Possible values are <tt>:top</tt>, <tt>:bottom</tt>, <tt>:before</tt> - # and <tt>:after</tt>. - # - # Using the <tt>:remove</tt> statement, you will be able to pass a block, but it will - # be ignored as there is no HTML passed for this statement. - # - # === Using blocks - # - # Without a block, +assert_select_rjs+ merely asserts that the response - # contains one or more RJS statements that replace or update content. - # - # With a block, +assert_select_rjs+ also selects all elements used in - # these statements and passes them to the block. Nested assertions are - # supported. - # - # Calling +assert_select_rjs+ with no arguments and using nested asserts - # asserts that the HTML content is returned by one or more RJS statements. - # Using +assert_select+ directly makes the same assertion on the content, - # but without distinguishing whether the content is returned in an HTML - # or JavaScript. - # - # ==== Examples - # - # # Replacing the element foo. - # # page.replace 'foo', ... - # assert_select_rjs :replace, "foo" - # - # # Replacing with the chained RJS proxy. - # # page[:foo].replace ... - # assert_select_rjs :chained_replace, 'foo' - # - # # Inserting into the element bar, top position. - # assert_select_rjs :insert, :top, "bar" - # - # # Remove the element bar - # assert_select_rjs :remove, "bar" - # - # # Changing the element foo, with an image. - # assert_select_rjs "foo" do - # assert_select "img[src=/images/logo.gif"" - # end - # - # # RJS inserts or updates a list with four items. - # assert_select_rjs do - # assert_select "ol>li", 4 - # end - # - # # The same, but shorter. - # assert_select "ol>li", 4 - def assert_select_rjs(*args, &block) - rjs_type = args.first.is_a?(Symbol) ? args.shift : nil - id = args.first.is_a?(String) ? args.shift : nil - - # If the first argument is a symbol, it's the type of RJS statement we're looking - # for (update, replace, insertion, etc). Otherwise, we're looking for just about - # any RJS statement. - if rjs_type - if rjs_type == :insert - position = args.shift - id = args.shift - insertion = "insert_#{position}".to_sym - raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion] - statement = "(#{RJS_STATEMENTS[insertion]})" - else - raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type] - statement = "(#{RJS_STATEMENTS[rjs_type]})" - end - else - statement = "#{RJS_STATEMENTS[:any]}" - end - - # Next argument we're looking for is the element identifier. If missing, we pick - # any element, otherwise we replace it in the statement. - pattern = Regexp.new( - id ? statement.gsub(RJS_ANY_ID, "\"#{id}\"") : statement - ) - - # Duplicate the body since the next step involves destroying it. - matches = nil - case rjs_type - when :remove, :show, :hide, :toggle - matches = @response.body.match(pattern) - else - @response.body.gsub(pattern) do |match| - html = unescape_rjs(match) - matches ||= [] - matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } - "" - end - end - - if matches - assert_block("") { true } # to count the assertion - if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) - begin - in_scope, @selected = @selected, matches - yield matches - ensure - @selected = in_scope - end - end - matches - else - # RJS statement not found. - case rjs_type - when :remove, :show, :hide, :toggle - flunk_message = "No RJS statement that #{rjs_type.to_s}s '#{id}' was rendered." - else - flunk_message = "No RJS statement that replaces or inserts HTML content." - end - flunk args.shift || flunk_message - end - end - - # :call-seq: - # assert_select_encoded(element?) { |elements| ... } - # - # Extracts the content of an element, treats it as encoded HTML and runs - # nested assertion on it. - # - # You typically call this method within another assertion to operate on - # all currently selected elements. You can also pass an element or array - # of elements. - # - # The content of each element is un-encoded, and wrapped in the root - # element +encoded+. It then calls the block with all un-encoded elements. - # - # ==== Examples - # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix) - # assert_select_feed :atom, 1.0 do - # # Select each entry item and then the title item - # assert_select "entry>title" do - # # Run assertions on the encoded title elements - # assert_select_encoded do - # assert_select "b" - # end - # end - # end - # - # - # # Selects all paragraph tags from within the description of an RSS feed - # assert_select_feed :rss, 2.0 do - # # Select description element of each feed item. - # assert_select "channel>item>description" do - # # Run assertions on the encoded elements. - # assert_select_encoded do - # assert_select "p" - # end - # end - # end - def assert_select_encoded(element = nil, &block) - case element - when Array - elements = element - when HTML::Node - elements = [element] - when nil - unless elements = @selected - raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" - end - else - raise ArgumentError, "Argument is optional, and may be node or array of nodes" - end - - fix_content = lambda do |node| - # Gets around a bug in the Rails 1.1 HTML parser. - node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) } - end - - selected = elements.map do |element| - text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join - root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root - css_select(root, "encoded:root", &block)[0] - end - - begin - old_selected, @selected = @selected, selected - assert_select ":root", &block - ensure - @selected = old_selected - end - end - - # :call-seq: - # assert_select_email { } - # - # Extracts the body of an email and runs nested assertions on it. - # - # You must enable deliveries for this assertion to work, use: - # ActionMailer::Base.perform_deliveries = true - # - # ==== Examples - # - # assert_select_email do - # assert_select "h1", "Email alert" - # end - # - # assert_select_email do - # items = assert_select "ol>li" - # items.each do - # # Work with items here... - # end - # end - # - def assert_select_email(&block) - deliveries = ActionMailer::Base.deliveries - assert !deliveries.empty?, "No e-mail in delivery list" - - for delivery in deliveries - for part in delivery.parts - if part["Content-Type"].to_s =~ /^text\/html\W/ - root = HTML::Document.new(part.body).root - assert_select root, ":root", &block - end - end - end - end - - protected - unless const_defined?(:RJS_STATEMENTS) - RJS_PATTERN_HTML = "\"((\\\\\"|[^\"])*)\"" - RJS_ANY_ID = "\"([^\"])*\"" - RJS_STATEMENTS = { - :chained_replace => "\\$\\(#{RJS_ANY_ID}\\)\\.replace\\(#{RJS_PATTERN_HTML}\\)", - :chained_replace_html => "\\$\\(#{RJS_ANY_ID}\\)\\.update\\(#{RJS_PATTERN_HTML}\\)", - :replace_html => "Element\\.update\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)", - :replace => "Element\\.replace\\(#{RJS_ANY_ID}, #{RJS_PATTERN_HTML}\\)" - } - [:remove, :show, :hide, :toggle].each do |action| - RJS_STATEMENTS[action] = "Element\\.#{action}\\(#{RJS_ANY_ID}\\)" - end - RJS_INSERTIONS = ["top", "bottom", "before", "after"] - RJS_INSERTIONS.each do |insertion| - RJS_STATEMENTS["insert_#{insertion}".to_sym] = "Element.insert\\(#{RJS_ANY_ID}, \\{ #{insertion}: #{RJS_PATTERN_HTML} \\}\\)" - end - RJS_STATEMENTS[:insert_html] = "Element.insert\\(#{RJS_ANY_ID}, \\{ (#{RJS_INSERTIONS.join('|')}): #{RJS_PATTERN_HTML} \\}\\)" - RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") - RJS_PATTERN_UNICODE_ESCAPED_CHAR = /\\u([0-9a-zA-Z]{4})/ - end - - # +assert_select+ and +css_select+ call this to obtain the content in the HTML - # page, or from all the RJS statements, depending on the type of response. - def response_from_page_or_rjs() - content_type = @response.content_type - - if content_type && Mime::JS =~ content_type - body = @response.body.dup - root = HTML::Node.new(nil) - - while true - next if body.sub!(RJS_STATEMENTS[:any]) do |match| - html = unescape_rjs(match) - matches = HTML::Document.new(html).root.children.select { |n| n.tag? } - root.children.concat matches - "" - end - break - end - - root - else - html_document.root - end - end - - # Unescapes a RJS string. - def unescape_rjs(rjs_string) - # RJS encodes double quotes and line breaks. - unescaped= rjs_string.gsub('\"', '"') - unescaped.gsub!(/\\\//, '/') - unescaped.gsub!('\n', "\n") - unescaped.gsub!('\076', '>') - unescaped.gsub!('\074', '<') - # RJS encodes non-ascii characters. - unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')} - unescaped - end - end - end -end diff --git a/actionpack/lib/action_controller/testing/assertions/tag.rb b/actionpack/lib/action_controller/testing/assertions/tag.rb deleted file mode 100644 index 80249e0e83..0000000000 --- a/actionpack/lib/action_controller/testing/assertions/tag.rb +++ /dev/null @@ -1,127 +0,0 @@ -module ActionController - module Assertions - # Pair of assertions to testing elements in the HTML output of the response. - module TagAssertions - # Asserts that there is a tag/node/element in the body of the response - # that meets all of the given conditions. The +conditions+ parameter must - # be a hash of any of the following keys (all are optional): - # - # * <tt>:tag</tt>: the node type must match the corresponding value - # * <tt>:attributes</tt>: a hash. The node's attributes must match the - # corresponding values in the hash. - # * <tt>:parent</tt>: a hash. The node's parent must match the - # corresponding hash. - # * <tt>:child</tt>: a hash. At least one of the node's immediate children - # must meet the criteria described by the hash. - # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must - # meet the criteria described by the hash. - # * <tt>:descendant</tt>: a hash. At least one of the node's descendants - # must meet the criteria described by the hash. - # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must - # meet the criteria described by the hash. - # * <tt>:after</tt>: a hash. The node must be after any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:before</tt>: a hash. The node must be before any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:children</tt>: a hash, for counting children of a node. Accepts - # the keys: - # * <tt>:count</tt>: either a number or a range which must equal (or - # include) the number of children that match. - # * <tt>:less_than</tt>: the number of matching children must be less - # than this number. - # * <tt>:greater_than</tt>: the number of matching children must be - # greater than this number. - # * <tt>:only</tt>: another hash consisting of the keys to use - # to match on the children, and only matching children will be - # counted. - # * <tt>:content</tt>: the textual content of the node must match the - # given value. This will not match HTML tags in the body of a - # tag--only text. - # - # Conditions are matched using the following algorithm: - # - # * if the condition is a string, it must be a substring of the value. - # * if the condition is a regexp, it must match the value. - # * if the condition is a number, the value must match number.to_s. - # * if the condition is +true+, the value must not be +nil+. - # * if the condition is +false+ or +nil+, the value must be +nil+. - # - # === Examples - # - # # Assert that there is a "span" tag - # assert_tag :tag => "span" - # - # # Assert that there is a "span" tag with id="x" - # assert_tag :tag => "span", :attributes => { :id => "x" } - # - # # Assert that there is a "span" tag using the short-hand - # assert_tag :span - # - # # Assert that there is a "span" tag with id="x" using the short-hand - # assert_tag :span, :attributes => { :id => "x" } - # - # # Assert that there is a "span" inside of a "div" - # assert_tag :tag => "span", :parent => { :tag => "div" } - # - # # Assert that there is a "span" somewhere inside a table - # assert_tag :tag => "span", :ancestor => { :tag => "table" } - # - # # Assert that there is a "span" with at least one "em" child - # assert_tag :tag => "span", :child => { :tag => "em" } - # - # # Assert that there is a "span" containing a (possibly nested) - # # "strong" tag. - # assert_tag :tag => "span", :descendant => { :tag => "strong" } - # - # # Assert that there is a "span" containing between 2 and 4 "em" tags - # # as immediate children - # assert_tag :tag => "span", - # :children => { :count => 2..4, :only => { :tag => "em" } } - # - # # Get funky: assert that there is a "div", with an "ul" ancestor - # # and an "li" parent (with "class" = "enum"), and containing a - # # "span" descendant that contains text matching /hello world/ - # assert_tag :tag => "div", - # :ancestor => { :tag => "ul" }, - # :parent => { :tag => "li", - # :attributes => { :class => "enum" } }, - # :descendant => { :tag => "span", - # :child => /hello world/ } - # - # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work - # with well-formed XHTML. They recognize a few tags as implicitly self-closing - # (like br and hr and such) but will not work correctly with tags - # that allow optional closing tags (p, li, td). <em>You must explicitly - # close all of your tags to use these assertions.</em> - def assert_tag(*opts) - clean_backtrace do - opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first - tag = find_tag(opts) - assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}" - end - end - - # Identical to +assert_tag+, but asserts that a matching tag does _not_ - # exist. (See +assert_tag+ for a full discussion of the syntax.) - # - # === Examples - # # Assert that there is not a "div" containing a "p" - # assert_no_tag :tag => "div", :descendant => { :tag => "p" } - # - # # Assert that an unordered list is empty - # assert_no_tag :tag => "ul", :descendant => { :tag => "li" } - # - # # Assert that there is not a "p" tag with between 1 to 3 "img" tags - # # as immediate children - # assert_no_tag :tag => "p", - # :children => { :count => 1..3, :only => { :tag => "img" } } - def assert_no_tag(*opts) - clean_backtrace do - opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first - tag = find_tag(opts) - assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}" - end - end - end - end -end diff --git a/actionpack/lib/action_controller/testing/integration.rb b/actionpack/lib/action_controller/testing/integration.rb index d51b9b63ff..af4ccb7837 100644 --- a/actionpack/lib/action_controller/testing/integration.rb +++ b/actionpack/lib/action_controller/testing/integration.rb @@ -2,8 +2,119 @@ require 'stringio' require 'uri' require 'active_support/test_case' +require 'rack/mock_session' +require 'rack/test/cookie_jar' + module ActionController module Integration #:nodoc: + module RequestHelpers + # Performs a GET request with the given parameters. + # + # - +path+: The URI (as a String) on which you want to perform a GET + # request. + # - +parameters+: The HTTP parameters that you want to pass. This may + # be +nil+, + # a Hash, or a String that is appropriately encoded + # (<tt>application/x-www-form-urlencoded</tt> or + # <tt>multipart/form-data</tt>). + # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will + # automatically be upcased, with the prefix 'HTTP_' added if needed. + # + # This method returns an Response object, which one can use to + # inspect the details of the response. Furthermore, if this method was + # called from an ActionController::IntegrationTest object, then that + # object's <tt>@response</tt> instance variable will point to the same + # response object. + # + # You can also perform POST, PUT, DELETE, and HEAD requests with +post+, + # +put+, +delete+, and +head+. + def get(path, parameters = nil, headers = nil) + process :get, path, parameters, headers + end + + # Performs a POST request with the given parameters. See get() for more + # details. + def post(path, parameters = nil, headers = nil) + process :post, path, parameters, headers + end + + # Performs a PUT request with the given parameters. See get() for more + # details. + def put(path, parameters = nil, headers = nil) + process :put, path, parameters, headers + end + + # Performs a DELETE request with the given parameters. See get() for + # more details. + def delete(path, parameters = nil, headers = nil) + process :delete, path, parameters, headers + end + + # Performs a HEAD request with the given parameters. See get() for more + # details. + def head(path, parameters = nil, headers = nil) + process :head, path, parameters, headers + end + + # Performs an XMLHttpRequest request with the given parameters, mirroring + # a request from the Prototype library. + # + # The request_method is :get, :post, :put, :delete or :head; the + # parameters are +nil+, a hash, or a url-encoded or multipart string; + # the headers are a hash. Keys are automatically upcased and prefixed + # with 'HTTP_' if not already. + def xml_http_request(request_method, path, parameters = nil, headers = nil) + headers ||= {} + headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') + process(request_method, path, parameters, headers) + end + alias xhr :xml_http_request + + # Follow a single redirect response. If the last response was not a + # redirect, an exception will be raised. Otherwise, the redirect is + # performed on the location header. + def follow_redirect! + raise "not a redirect! #{status} #{status_message}" unless redirect? + get(response.location) + status + end + + # Performs a request using the specified method, following any subsequent + # redirect. Note that the redirects are followed until the response is + # not a redirect--this means you may run into an infinite loop if your + # redirect loops back to itself. + def request_via_redirect(http_method, path, parameters = nil, headers = nil) + process(http_method, path, parameters, headers) + follow_redirect! while redirect? + status + end + + # Performs a GET request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def get_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:get, path, parameters, headers) + end + + # Performs a POST request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def post_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:post, path, parameters, headers) + end + + # Performs a PUT request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def put_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:put, path, parameters, headers) + end + + # Performs a DELETE request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def delete_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:delete, path, parameters, headers) + end + end + # An integration Session instance represents a set of requests and responses # performed sequentially by some virtual user. Because you can instantiate # multiple sessions and run them side-by-side, you can also mimic (to some @@ -13,24 +124,20 @@ module ActionController # IntegrationTest#open_session, rather than instantiating # Integration::Session directly. class Session + DEFAULT_HOST = "www.example.com" + include Test::Unit::Assertions - include ActionController::TestCase::Assertions + include ActionDispatch::Assertions include ActionController::TestProcess + include RequestHelpers - # Rack application to use - attr_accessor :application - - # The integer HTTP status code of the last request. - attr_reader :status - - # The status message that accompanied the status code of the last request. - attr_reader :status_message - - # The body of the last request. - attr_reader :body + %w( status status_message headers body redirect? ).each do |method| + delegate method, :to => :response, :allow_nil => true + end - # The URI of the last request. - attr_reader :path + %w( path ).each do |method| + delegate method, :to => :request, :allow_nil => true + end # The hostname used in the last request. attr_accessor :host @@ -43,10 +150,9 @@ module ActionController # A map of the cookies returned by the last response, and which will be # sent with the next request. - attr_reader :cookies - - # A map of the headers returned by the last response. - attr_reader :headers + def cookies + @mock_session.cookie_jar + end # A reference to the controller instance used by the last request. attr_reader :controller @@ -60,12 +166,9 @@ module ActionController # A running counter of the number of requests processed. attr_accessor :request_count - class MultiPartNeededException < Exception - end - # Create and initialize a new Session instance. def initialize(app = nil) - @application = app || ActionController::Dispatcher.new + @app = app || ActionController::Dispatcher.new reset! end @@ -75,14 +178,12 @@ module ActionController # # session.reset! def reset! - @status = @path = @headers = nil - @result = @status_message = nil @https = false - @cookies = {} + @mock_session = Rack::MockSession.new(@app, DEFAULT_HOST) @controller = @request = @response = nil @request_count = 0 - self.host = "www.example.com" + self.host = DEFAULT_HOST self.remote_addr = "127.0.0.1" self.accept = "text/xml,application/xml,application/xhtml+xml," + "text/html;q=0.9,text/plain;q=0.8,image/png," + @@ -124,117 +225,6 @@ module ActionController @host = name end - # Follow a single redirect response. If the last response was not a - # redirect, an exception will be raised. Otherwise, the redirect is - # performed on the location header. - def follow_redirect! - raise "not a redirect! #{@status} #{@status_message}" unless redirect? - get(interpret_uri(headers['location'])) - status - end - - # Performs a request using the specified method, following any subsequent - # redirect. Note that the redirects are followed until the response is - # not a redirect--this means you may run into an infinite loop if your - # redirect loops back to itself. - def request_via_redirect(http_method, path, parameters = nil, headers = nil) - send(http_method, path, parameters, headers) - follow_redirect! while redirect? - status - end - - # Performs a GET request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def get_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:get, path, parameters, headers) - end - - # Performs a POST request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def post_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:post, path, parameters, headers) - end - - # Performs a PUT request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def put_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:put, path, parameters, headers) - end - - # Performs a DELETE request, following any subsequent redirect. - # See +request_via_redirect+ for more information. - def delete_via_redirect(path, parameters = nil, headers = nil) - request_via_redirect(:delete, path, parameters, headers) - end - - # Returns +true+ if the last response was a redirect. - def redirect? - status/100 == 3 - end - - # Performs a GET request with the given parameters. - # - # - +path+: The URI (as a String) on which you want to perform a GET - # request. - # - +parameters+: The HTTP parameters that you want to pass. This may - # be +nil+, - # a Hash, or a String that is appropriately encoded - # (<tt>application/x-www-form-urlencoded</tt> or - # <tt>multipart/form-data</tt>). - # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will - # automatically be upcased, with the prefix 'HTTP_' added if needed. - # - # This method returns an Response object, which one can use to - # inspect the details of the response. Furthermore, if this method was - # called from an ActionController::IntegrationTest object, then that - # object's <tt>@response</tt> instance variable will point to the same - # response object. - # - # You can also perform POST, PUT, DELETE, and HEAD requests with +post+, - # +put+, +delete+, and +head+. - def get(path, parameters = nil, headers = nil) - process :get, path, parameters, headers - end - - # Performs a POST request with the given parameters. See get() for more - # details. - def post(path, parameters = nil, headers = nil) - process :post, path, parameters, headers - end - - # Performs a PUT request with the given parameters. See get() for more - # details. - def put(path, parameters = nil, headers = nil) - process :put, path, parameters, headers - end - - # Performs a DELETE request with the given parameters. See get() for - # more details. - def delete(path, parameters = nil, headers = nil) - process :delete, path, parameters, headers - end - - # Performs a HEAD request with the given parameters. See get() for more - # details. - def head(path, parameters = nil, headers = nil) - process :head, path, parameters, headers - end - - # Performs an XMLHttpRequest request with the given parameters, mirroring - # a request from the Prototype library. - # - # The request_method is :get, :post, :put, :delete or :head; the - # parameters are +nil+, a hash, or a url-encoded or multipart string; - # the headers are a hash. Keys are automatically upcased and prefixed - # with 'HTTP_' if not already. - def xml_http_request(request_method, path, parameters = nil, headers = nil) - headers ||= {} - headers['X-Requested-With'] = 'XMLHttpRequest' - headers['Accept'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ') - process(request_method, path, parameters, headers) - end - alias xhr :xml_http_request - # Returns the URL for the given options, according to the rules specified # in the application's routes. def url_for(options) @@ -244,137 +234,54 @@ module ActionController end private - # Tailors the session based on the given URI, setting the HTTPS value - # and the hostname. - def interpret_uri(path) - location = URI.parse(path) - https! URI::HTTPS === location if location.scheme - host! location.host if location.host - location.query ? "#{location.path}?#{location.query}" : location.path - end # Performs the actual request. - def process(method, path, parameters = nil, headers = nil) - data = requestify(parameters) - path = interpret_uri(path) if path =~ %r{://} - path = "/#{path}" unless path[0] == ?/ - @path = path - env = {} - - if method == :get - env["QUERY_STRING"] = data - data = nil + def process(method, path, parameters = nil, rack_environment = nil) + if path =~ %r{://} + location = URI.parse(path) + https! URI::HTTPS === location if location.scheme + host! location.host if location.host + path = location.query ? "#{location.path}?#{location.query}" : location.path end - env["QUERY_STRING"] ||= "" + [ControllerCapture, ActionController::ProcessWithTest].each do |mod| + unless ActionController::Base < mod + ActionController::Base.class_eval { include mod } + end + end - data = data.is_a?(IO) ? data : StringIO.new(data || '') + opts = { + :method => method, + :params => parameters, - env.update( - "REQUEST_METHOD" => method.to_s.upcase, "SERVER_NAME" => host, "SERVER_PORT" => (https? ? "443" : "80"), "HTTPS" => https? ? "on" : "off", "rack.url_scheme" => https? ? "https" : "http", - "SCRIPT_NAME" => "", "REQUEST_URI" => path, "PATH_INFO" => path, "HTTP_HOST" => host, "REMOTE_ADDR" => remote_addr, "CONTENT_TYPE" => "application/x-www-form-urlencoded", - "CONTENT_LENGTH" => data ? data.length.to_s : nil, - "HTTP_COOKIE" => encode_cookies, - "HTTP_ACCEPT" => accept, - - "rack.version" => [0,1], - "rack.input" => data, - "rack.errors" => StringIO.new, - "rack.multithread" => true, - "rack.multiprocess" => true, - "rack.run_once" => false, - - "rack.test" => true - ) - - (headers || {}).each do |key, value| - key = key.to_s.upcase.gsub(/-/, "_") - key = "HTTP_#{key}" unless env.has_key?(key) || key =~ /^HTTP_/ - env[key] = value - end + "HTTP_ACCEPT" => accept + } + env = Rack::MockRequest.env_for(path, opts) - [ControllerCapture, ActionController::ProcessWithTest].each do |mod| - unless ActionController::Base < mod - ActionController::Base.class_eval { include mod } - end + (rack_environment || {}).each do |key, value| + env[key] = value end - ActionController::Base.clear_last_instantiation! - - app = @application - # Rack::Lint doesn't accept String headers or bodies in Ruby 1.9 - unless RUBY_VERSION >= '1.9.0' && Rack.release <= '0.9.0' - app = Rack::Lint.new(app) + @controller = ActionController::Base.capture_instantiation do + @mock_session.request(URI.parse(path), env) end - status, headers, body = app.call(env) @request_count += 1 - + @request = ActionDispatch::Request.new(env) + @response = ActionDispatch::TestResponse.from_response(@mock_session.last_response) @html_document = nil - @status = status.to_i - @status_message = ActionDispatch::StatusCodes::STATUS_CODES[@status] - - @headers = Rack::Utils::HeaderHash.new(headers) - - (@headers['Set-Cookie'] || "").split("\n").each do |cookie| - name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2] - @cookies[name] = value - end - - if body.is_a?(String) - @body_parts = [body] - @body = body - else - @body_parts = [] - body.each { |part| @body_parts << part.to_s } - @body = @body_parts.join - end - - if @controller = ActionController::Base.last_instantiation - @request = @controller.request - @response = @controller.response - @controller.send(:set_test_assigns) - else - # Decorate responses from Rack Middleware and Rails Metal - # as an Response for the purposes of integration testing - @response = ActionDispatch::Response.new - @response.status = status.to_s - @response.headers.replace(@headers) - @response.body = @body_parts - end - - # Decorate the response with the standard behavior of the - # TestResponse so that things like assert_response can be - # used in integration tests. - @response.extend(TestResponseBehavior) - - return @status - rescue MultiPartNeededException - boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1" - status = process(method, path, - multipart_body(parameters, boundary), - (headers || {}).merge( - {"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}"})) - return status - end - - # Encode the cookies hash in a format suitable for passing to a - # request. - def encode_cookies - cookies.inject("") do |string, (name, value)| - string << "#{name}=#{value}; " - end + return response.status end # Get a temporary URL writer object @@ -389,97 +296,29 @@ module ActionController } UrlRewriter.new(ActionDispatch::Request.new(env), {}) end - - def name_with_prefix(prefix, name) - prefix ? "#{prefix}[#{name}]" : name.to_s - end - - # Convert the given parameters to a request string. The parameters may - # be a string, +nil+, or a Hash. - def requestify(parameters, prefix=nil) - if TestUploadedFile === parameters - raise MultiPartNeededException - elsif Hash === parameters - return nil if parameters.empty? - parameters.map { |k,v| - requestify(v, name_with_prefix(prefix, k)) - }.join("&") - elsif Array === parameters - parameters.map { |v| - requestify(v, name_with_prefix(prefix, "")) - }.join("&") - elsif prefix.nil? - parameters - else - "#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}" - end - end - - def multipart_requestify(params, first=true) - returning Hash.new do |p| - params.each do |key, value| - k = first ? CGI.escape(key.to_s) : "[#{CGI.escape(key.to_s)}]" - if Hash === value - multipart_requestify(value, false).each do |subkey, subvalue| - p[k + subkey] = subvalue - end - else - p[k] = value - end - end - end - end - - def multipart_body(params, boundary) - multipart_requestify(params).map do |key, value| - if value.respond_to?(:original_filename) - File.open(value.path, "rb") do |f| - f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) - - <<-EOF ---#{boundary}\r -Content-Disposition: form-data; name="#{key}"; filename="#{CGI.escape(value.original_filename)}"\r -Content-Type: #{value.content_type}\r -Content-Length: #{File.stat(value.path).size}\r -\r -#{f.read}\r -EOF - end - else -<<-EOF ---#{boundary}\r -Content-Disposition: form-data; name="#{key}"\r -\r -#{value}\r -EOF - end - end.join("")+"--#{boundary}--\r" - end end # A module used to extend ActionController::Base, so that integration tests # can capture the controller used to satisfy a request. module ControllerCapture #:nodoc: - def self.included(base) - base.extend(ClassMethods) - base.class_eval do - class << self - alias_method_chain :new, :capture - end - end + extend ActiveSupport::Concern + + included do + alias_method_chain :initialize, :capture + end + + def initialize_with_capture(*args) + initialize_without_capture + self.class.last_instantiation ||= self end module ClassMethods #:nodoc: mattr_accessor :last_instantiation - def clear_last_instantiation! + def capture_instantiation self.last_instantiation = nil - end - - def new_with_capture(*args) - controller = new_without_capture(*args) - self.last_instantiation ||= controller - controller + yield + return last_instantiation end end end @@ -513,8 +352,8 @@ EOF # By default, a single session is automatically created for you, but you # can use this method to open multiple sessions that ought to be tested # simultaneously. - def open_session(application = nil) - session = Integration::Session.new(application) + def open_session(app = nil) + session = Integration::Session.new(app) # delegate the fixture accessors back to the test instance extras = Module.new { attr_accessor :delegate, :test_result } @@ -636,54 +475,5 @@ EOF # end class IntegrationTest < ActiveSupport::TestCase include Integration::Runner - - # Work around a bug in test/unit caused by the default test being named - # as a symbol (:default_test), which causes regex test filters - # (like "ruby test.rb -n /foo/") to fail because =~ doesn't work on - # symbols. - def initialize(name) #:nodoc: - super(name.to_s) - end - - # Work around test/unit's requirement that every subclass of TestCase have - # at least one test method. Note that this implementation extends to all - # subclasses, as well, so subclasses of IntegrationTest may also exist - # without any test methods. - def run(*args) #:nodoc: - return if @method_name == "default_test" - super - end - - # Because of how use_instantiated_fixtures and use_transactional_fixtures - # are defined, we need to treat them as special cases. Otherwise, users - # would potentially have to set their values for both Test::Unit::TestCase - # ActionController::IntegrationTest, since by the time the value is set on - # TestCase, IntegrationTest has already been defined and cannot inherit - # changes to those variables. So, we make those two attributes - # copy-on-write. - - class << self - def use_transactional_fixtures=(flag) #:nodoc: - @_use_transactional_fixtures = true - @use_transactional_fixtures = flag - end - - def use_instantiated_fixtures=(flag) #:nodoc: - @_use_instantiated_fixtures = true - @use_instantiated_fixtures = flag - end - - def use_transactional_fixtures #:nodoc: - @_use_transactional_fixtures ? - @use_transactional_fixtures : - superclass.use_transactional_fixtures - end - - def use_instantiated_fixtures #:nodoc: - @_use_instantiated_fixtures ? - @use_instantiated_fixtures : - superclass.use_instantiated_fixtures - end - end end end diff --git a/actionpack/lib/action_controller/testing/process.rb b/actionpack/lib/action_controller/testing/process.rb index 7e2857614c..9647f8ce45 100644 --- a/actionpack/lib/action_controller/testing/process.rb +++ b/actionpack/lib/action_controller/testing/process.rb @@ -1,94 +1,13 @@ require 'rack/session/abstract/id' -module ActionController #:nodoc: - class TestRequest < ActionDispatch::Request #:nodoc: - attr_accessor :cookies, :session_options - attr_accessor :query_parameters, :path, :session - attr_accessor :host - - def self.new(env = {}) - super - end +require 'active_support/core_ext/object/conversions' +module ActionController #:nodoc: + class TestRequest < ActionDispatch::TestRequest #:nodoc: def initialize(env = {}) - super(Rack::MockRequest.env_for("/").merge(env)) - - @query_parameters = {} - @session = TestSession.new - default_rack_options = Rack::Session::Abstract::ID::DEFAULT_OPTIONS - @session_options ||= {:id => generate_sid(default_rack_options[:sidbits])}.merge(default_rack_options) - - initialize_default_values - initialize_containers - end - - def reset_session - @session.reset - end - - # Wraps raw_post in a StringIO. - def body_stream #:nodoc: - StringIO.new(raw_post) - end - - # Either the RAW_POST_DATA environment variable or the URL-encoded request - # parameters. - def raw_post - @env['RAW_POST_DATA'] ||= begin - data = url_encoded_request_parameters - data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding) - data - end - end - - def port=(number) - @env["SERVER_PORT"] = number.to_i - end - - def action=(action_name) - @query_parameters.update({ "action" => action_name }) - @parameters = nil - end - - # Used to check AbstractRequest's request_uri functionality. - # Disables the use of @path and @request_uri so superclass can handle those. - def set_REQUEST_URI(value) - @env["REQUEST_URI"] = value - @request_uri = nil - @path = nil - end - - def request_uri=(uri) - @request_uri = uri - @path = uri.split("?").first - end - - def request_method=(method) - @request_method = method - end - - def accept=(mime_types) - @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") - @accepts = nil - end - - def if_modified_since=(last_modified) - @env["HTTP_IF_MODIFIED_SINCE"] = last_modified - end - - def if_none_match=(etag) - @env["HTTP_IF_NONE_MATCH"] = etag - end - - def remote_addr=(addr) - @env['REMOTE_ADDR'] = addr - end - - def request_uri(*args) - @request_uri || super() - end + super - def path(*args) - @path || super() + self.session = TestSession.new + self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => ActiveSupport::SecureRandom.hex(16)) end def assign_parameters(controller_path, action, parameters) @@ -108,247 +27,46 @@ module ActionController #:nodoc: path_parameters[key.to_s] = value end end - raw_post # populate env['RAW_POST_DATA'] - @parameters = nil # reset TestRequest#parameters to use the new path_parameters - end - def recycle! - @env["action_controller.request.request_parameters"] = {} - self.query_parameters = {} - self.path_parameters = {} - @headers, @request_method, @accepts, @content_type = nil, nil, nil, nil - end - - def user_agent=(user_agent) - @env['HTTP_USER_AGENT'] = user_agent - end - - private - def generate_sid(sidbits) - "%0#{sidbits / 4}x" % rand(2**sidbits - 1) - end - - def initialize_containers - @cookies = {} - end - - def initialize_default_values - @host = "test.host" - @request_uri = "/" - @env['HTTP_USER_AGENT'] = "Rails Testing" - @env['REMOTE_ADDR'] = "0.0.0.0" - @env["SERVER_PORT"] = 80 - @env['REQUEST_METHOD'] = "GET" - end - - def url_encoded_request_parameters - params = self.request_parameters.dup - - %w(controller action only_path).each do |k| - params.delete(k) - params.delete(k.to_sym) - end + params = self.request_parameters.dup - params.to_query + %w(controller action only_path).each do |k| + params.delete(k) + params.delete(k.to_sym) end - end - - # A refactoring of TestResponse to allow the same behavior to be applied - # to the "real" CgiResponse class in integration tests. - module TestResponseBehavior #:nodoc: - # The response code of the request - def response_code - status.to_s[0,3].to_i rescue 0 - end - - # Returns a String to ensure compatibility with Net::HTTPResponse - def code - status.to_s.split(' ')[0] - end - - def message - status.to_s.split(' ',2)[1] - end - - # Was the response successful? - def success? - (200..299).include?(response_code) - end - # Was the URL not found? - def missing? - response_code == 404 + data = params.to_query + @env['CONTENT_LENGTH'] = data.length.to_s + @env['rack.input'] = StringIO.new(data) end - # Were we redirected? - def redirect? - (300..399).include?(response_code) - end - - # Was there a server-side error? - def error? - (500..599).include?(response_code) - end - - alias_method :server_error?, :error? - - # Was there a client client? - def client_error? - (400..499).include?(response_code) - end - - # Returns the redirection location or nil - def redirect_url - headers['Location'] - end - - # Does the redirect location match this regexp pattern? - def redirect_url_match?( pattern ) - return false if redirect_url.nil? - p = Regexp.new(pattern) if pattern.class == String - p = pattern if pattern.class == Regexp - return false if p.nil? - p.match(redirect_url) != nil - end - - # Returns the template of the file which was used to - # render this response (or nil) - def rendered - template.instance_variable_get(:@_rendered) - end - - # A shortcut to the flash. Returns an empty hash if no session flash exists. - def flash - session['flash'] || {} - end - - # Do we have a flash? - def has_flash? - !flash.empty? - end - - # Do we have a flash that has contents? - def has_flash_with_contents? - !flash.empty? - end - - # Does the specified flash object exist? - def has_flash_object?(name=nil) - !flash[name].nil? - end - - # Does the specified object exist in the session? - def has_session_object?(name=nil) - !session[name].nil? - end - - # A shortcut to the template.assigns - def template_objects - template.assigns || {} - end - - # Does the specified template object exist? - def has_template_object?(name=nil) - !template_objects[name].nil? - end - - # Returns the response cookies, converted to a Hash of (name => value) pairs - # - # assert_equal 'AuthorOfNewPage', r.cookies['author'] - def cookies - cookies = {} - Array(headers['Set-Cookie']).each do |cookie| - key, value = cookie.split(";").first.split("=").map {|val| Rack::Utils.unescape(val)} - cookies[key] = value - end - cookies - end - - # Returns binary content (downloadable file), converted to a String - def binary_content - raise "Response body is not a Proc: #{body_parts.inspect}" unless body_parts.kind_of?(Proc) - require 'stringio' - - sio = StringIO.new - body_parts.call(self, sio) - - sio.rewind - sio.read - end - end - - # Integration test methods such as ActionController::Integration::Session#get - # and ActionController::Integration::Session#post return objects of class - # TestResponse, which represent the HTTP response results of the requested - # controller actions. - # - # See Response for more information on controller response objects. - class TestResponse < ActionDispatch::Response - include TestResponseBehavior - def recycle! - body_parts.clear - headers.delete('ETag') - headers.delete('Last-Modified') + @formats = nil + @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } + @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + @env['action_dispatch.request.query_parameters'] = {} end end - class TestSession < Hash #:nodoc: - attr_accessor :session_id - - def initialize(attributes = nil) - reset_session_id - replace_attributes(attributes) - end - - def reset - reset_session_id - replace_attributes({ }) - end - - def data - to_hash - end - - def [](key) - super(key.to_s) - end - - def []=(key, value) - super(key.to_s, value) - end - - def update(hash = nil) - if hash.nil? - ActiveSupport::Deprecation.warn('use replace instead', caller) - replace({}) - else - super(hash) - end - end - - def delete(key = nil) - if key.nil? - ActiveSupport::Deprecation.warn('use clear instead', caller) - clear - else - super(key.to_s) - end - end + class TestResponse < ActionDispatch::TestResponse + def recycle! + @status = 200 + @header = Rack::Utils::HeaderHash.new(DEFAULT_HEADERS) + @writer = lambda { |x| @body << x } + @block = nil + @length = 0 + @body = [] - def close - ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller) + @request = @template = nil end + end - private - - def reset_session_id - @session_id = '' - end + class TestSession < ActionDispatch::Session::AbstractStore::SessionHash #:nodoc: + DEFAULT_OPTIONS = ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS - def replace_attributes(attributes = nil) - attributes ||= {} - replace(attributes.stringify_keys) + def initialize(session = {}) + replace(session.stringify_keys) + @loaded = true end end @@ -363,34 +81,7 @@ module ActionController #:nodoc: # # Pass a true third parameter to ensure the uploaded file is opened in binary mode (only required for Windows): # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary) - require 'tempfile' - class TestUploadedFile - # The filename, *not* including the path, of the "uploaded" file - attr_reader :original_filename - - # The content type of the "uploaded" file - attr_accessor :content_type - - def initialize(path, content_type = Mime::TEXT, binary = false) - raise "#{path} file does not exist" unless File.exist?(path) - @content_type = content_type - @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 } - @tempfile = Tempfile.new(@original_filename) - @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding) - @tempfile.binmode if binary - FileUtils.copy_file(path, @tempfile.path) - end - - def path #:nodoc: - @tempfile.path - end - - alias local_path path - - def method_missing(method_name, *args, &block) #:nodoc: - @tempfile.__send__(method_name, *args, &block) - end - end + TestUploadedFile = Rack::Utils::Multipart::UploadedFile module TestProcess def self.included(base) @@ -433,9 +124,7 @@ module ActionController #:nodoc: @response.recycle! @html_document = nil - @request.env['REQUEST_METHOD'] = http_method - - @request.action = action.to_s + @request.request_method = http_method parameters ||= {} @request.assign_parameters(@controller.class.controller_path, action.to_s, parameters) @@ -445,7 +134,19 @@ module ActionController #:nodoc: build_request_uri(action, parameters) Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest - @controller.process_with_test(@request, @response) + + env = @request.env + app = @controller + + # TODO: Enable Lint + # app = Rack::Lint.new(app) + + status, headers, body = app.action(action, env) + response = Rack::MockResponse.new(status, headers, body) + + @response.request, @response.template = @request, @controller.template + @response.status, @response.headers, @response.body = response.status, response.headers, response.body + @response end def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) @@ -459,11 +160,13 @@ module ActionController #:nodoc: alias xhr :xml_http_request def assigns(key = nil) - if key.nil? - @response.template.assigns - else - @response.template.assigns[key.to_s] + assigns = {} + @controller.instance_variable_names.each do |ivar| + next if ActionController::Base.protected_instance_variables.include?(ivar) + assigns[ivar[1..-1]] = @controller.instance_variable_get(ivar) end + + key.nil? ? assigns : assigns[key.to_s] end def session @@ -471,7 +174,7 @@ module ActionController #:nodoc: end def flash - @response.flash + @request.flash end def cookies @@ -488,7 +191,7 @@ module ActionController #:nodoc: options.update(:only_path => true, :action => action) url = ActionController::UrlRewriter.new(@request, parameters) - @request.set_REQUEST_URI(url.rewrite(options)) + @request.request_uri = url.rewrite(options) end end @@ -561,11 +264,14 @@ module ActionController #:nodoc: module ProcessWithTest #:nodoc: def self.included(base) - base.class_eval { attr_reader :assigns } + base.class_eval { + attr_reader :assigns + alias_method_chain :process, :test + } end def process_with_test(*args) - process(*args).tap { set_test_assigns } + process_without_test(*args).tap { set_test_assigns } end private @@ -574,7 +280,7 @@ module ActionController #:nodoc: (instance_variable_names - self.class.protected_instance_variables).each do |var| name, value = var[1..-1], instance_variable_get(var) @assigns[name] = value - response.template.assigns[name] = value if response + @template.assigns[name] = value if response end end end diff --git a/actionpack/lib/action_controller/testing/process2.rb b/actionpack/lib/action_controller/testing/process2.rb new file mode 100644 index 0000000000..1c6fd2d80a --- /dev/null +++ b/actionpack/lib/action_controller/testing/process2.rb @@ -0,0 +1,74 @@ +require "action_controller/testing/process" + +module ActionController + module TestProcess + + # Executes a request simulating GET HTTP method and set/volley the response + def get(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "GET") + end + + # Executes a request simulating POST HTTP method and set/volley the response + def post(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "POST") + end + + # Executes a request simulating PUT HTTP method and set/volley the response + def put(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "PUT") + end + + # Executes a request simulating DELETE HTTP method and set/volley the response + def delete(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "DELETE") + end + + # Executes a request simulating HEAD HTTP method and set/volley the response + def head(action, parameters = nil, session = nil, flash = nil) + process(action, parameters, session, flash, "HEAD") + end + + def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') + # Sanity check for required instance variables so we can give an + # understandable error message. + %w(@controller @request @response).each do |iv_name| + if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil? + raise "#{iv_name} is nil: make sure you set it in your test's setup method." + end + end + + @request.recycle! + @response.recycle! + @controller.response_body = nil + @controller.formats = nil + @controller.params = nil + + @html_document = nil + @request.env['REQUEST_METHOD'] = http_method + + parameters ||= {} + @request.assign_parameters(@controller.class.controller_path, action.to_s, parameters) + + @request.session = ActionController::TestSession.new(session) unless session.nil? + @request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash + + @controller.request = @request + @controller.params.merge!(parameters) + build_request_uri(action, parameters) + # Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest + @controller.process_with_new_base_test(@request, @response) + @response + end + + def build_request_uri(action, parameters) + unless @request.env['REQUEST_URI'] + options = @controller.__send__(:rewrite_options, parameters) + options.update(:only_path => true, :action => action) + + url = ActionController::UrlRewriter.new(@request, parameters) + @request.request_uri = url.rewrite(options) + end + end + + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/testing/test_case.rb b/actionpack/lib/action_controller/testing/test_case.rb index b020b755a0..7b4eda58e5 100644 --- a/actionpack/lib/action_controller/testing/test_case.rb +++ b/actionpack/lib/action_controller/testing/test_case.rb @@ -105,20 +105,7 @@ module ActionController class TestCase < ActiveSupport::TestCase include TestProcess - module Assertions - %w(response selector tag dom routing model).each do |kind| - include ActionController::Assertions.const_get("#{kind.camelize}Assertions") - end - - def clean_backtrace(&block) - yield - rescue ActiveSupport::TestCase::Assertion => error - framework_path = Regexp.new(File.expand_path("#{File.dirname(__FILE__)}/assertions")) - error.backtrace.reject! { |line| File.expand_path(line) =~ framework_path } - raise - end - end - include Assertions + include ActionDispatch::Assertions # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline # (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index ae20f9947c..a992f7d912 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -73,7 +73,7 @@ module HTML # Specifies the default Set of tags that the #sanitize helper will allow unscathed. self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub - sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dt dd abbr + sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr acronym a img blockquote del ins)) # Specifies the default Set of html attributes that the #sanitize helper will leave |