Upgrade to Rails 2.0.2

Upgraded to Rails 2.0.2, except that we maintain

   vendor/rails/actionpack/lib/action_controller/routing.rb

from Rail 1.2.6 (at least for now), so that Routes don't change. We still
get to enjoy Rails's many new features.

Also fixed a bug in Chunk-handling: disable WikiWord processing in tags (for real this time).
This commit is contained in:
Jacques Distler 2007-12-21 01:48:59 -06:00
parent 0f6889e09f
commit 6873fc8026
1083 changed files with 52810 additions and 41058 deletions

View file

@ -1,4 +1,3 @@
require 'test/unit'
require 'test/unit/assertions'
module ActionController #:nodoc:
@ -9,7 +8,7 @@ module ActionController #:nodoc:
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
@ -17,7 +16,7 @@ module ActionController #:nodoc:
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing.
# appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
@ -39,44 +38,32 @@ module ActionController #:nodoc:
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes methods straight in the test case.
# Example:
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
def self.included(klass)
klass.class_eval do
include ActionController::Assertions::ResponseAssertions
include ActionController::Assertions::SelectorAssertions
include ActionController::Assertions::RoutingAssertions
include ActionController::Assertions::TagAssertions
include ActionController::Assertions::DomAssertions
include ActionController::Assertions::ModelAssertions
include ActionController::Assertions::DeprecatedAssertions
%w(response selector tag dom routing model).each do |kind|
require "action_controller/assertions/#{kind}_assertions"
klass.module_eval { include const_get("#{kind.camelize}Assertions") }
end
end
def clean_backtrace(&block)
yield
rescue Test::Unit::AssertionFailedError => e
path = File.expand_path(__FILE__)
raise Test::Unit::AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
rescue Test::Unit::AssertionFailedError => 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
end
require File.dirname(__FILE__) + '/assertions/response_assertions'
require File.dirname(__FILE__) + '/assertions/selector_assertions'
require File.dirname(__FILE__) + '/assertions/tag_assertions'
require File.dirname(__FILE__) + '/assertions/dom_assertions'
require File.dirname(__FILE__) + '/assertions/routing_assertions'
require File.dirname(__FILE__) + '/assertions/model_assertions'
require File.dirname(__FILE__) + '/assertions/deprecated_assertions'
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
include ActionController::Assertions
end
end
end
end

View file

@ -1,228 +0,0 @@
require 'rexml/document'
module ActionController #:nodoc:
module Assertions #:nodoc:
module DeprecatedAssertions #:nodoc:
def assert_success(message=nil) #:nodoc:
assert_response(:success, message)
end
deprecate :assert_success => "use assert_response(:success)"
def assert_redirect(message=nil) #:nodoc:
assert_response(:redirect, message)
end
deprecate :assert_redirect => "use assert_response(:redirect)"
def assert_rendered_file(expected=nil, message=nil) #:nodoc:
assert_template(expected, message)
end
deprecate :assert_rendered_file => :assert_template
# ensure that the session has an object with the specified name
def assert_session_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the session <?>", key, @response.session)
assert_block(msg) { @response.has_session_object?(key) }
end
deprecate :assert_session_has => "use assert(@response.has_session_object?(key))"
# ensure that the session has no object with the specified name
def assert_session_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the session <?>", key, @response.session)
assert_block(msg) { !@response.has_session_object?(key) }
end
deprecate :assert_session_has_no => "use assert(!@response.has_session_object?(key))"
def assert_session_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, @response.session[key])
assert_block(msg) { expected == @response.session[key] }
end
deprecate :assert_session_equal => "use assert_equal(expected, @response[key])"
# -- cookie assertions ---------------------------------------------------
def assert_no_cookie(key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
msg = build_message(message, "<?> not expected in cookies['?']", actual, key)
assert_block(msg) { actual.nil? or actual.empty? }
end
deprecate :assert_no_cookie => "use assert(!@response.cookies.key?(key))"
def assert_cookie_equal(expected = nil, key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
actual = actual.first if actual
msg = build_message(message, "<?> expected in cookies['?'] but was <?>", expected, key, actual)
assert_block(msg) { expected == actual }
end
deprecate :assert_cookie_equal => "use assert(@response.cookies.key?(key))"
# -- flash assertions ---------------------------------------------------
# ensure that the flash has an object with the specified name
def assert_flash_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the flash <?>", key, @response.flash)
assert_block(msg) { @response.has_flash_object?(key) }
end
deprecate :assert_flash_has => "use assert(@response.has_flash_object?(key))"
# ensure that the flash has no object with the specified name
def assert_flash_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the flash <?>", key, @response.flash)
assert_block(msg) { !@response.has_flash_object?(key) }
end
deprecate :assert_flash_has_no => "use assert(!@response.has_flash_object?(key))"
# ensure the flash exists
def assert_flash_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash does not exist <?>", @response.session['flash'] )
assert_block(msg) { @response.has_flash? }
end
deprecate :assert_flash_exists => "use assert(@response.has_flash?)"
# ensure the flash does not exist
def assert_flash_not_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash exists <?>", @response.flash)
assert_block(msg) { !@response.has_flash? }
end
deprecate :assert_flash_not_exists => "use assert(!@response.has_flash?)"
# ensure the flash is empty but existent
def assert_flash_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is not empty <?>", @response.flash)
assert_block(msg) { !@response.has_flash_with_contents? }
end
deprecate :assert_flash_empty => "use assert(!@response.has_flash_with_contents?)"
# ensure the flash is not empty
def assert_flash_not_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is empty")
assert_block(msg) { @response.has_flash_with_contents? }
end
deprecate :assert_flash_not_empty => "use assert(@response.has_flash_with_contents?)"
def assert_flash_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, @response.flash[key])
assert_block(msg) { expected == @response.flash[key] }
end
deprecate :assert_flash_equal => "use assert_equal(expected, @response.flash[key])"
# ensure our redirection url is an exact match
def assert_redirect_url(url=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> is not the redirected location <?>", url, @response.redirect_url)
assert_block(msg) { @response.redirect_url == url }
end
deprecate :assert_redirect_url => "use assert_equal(url, @response.redirect_url)"
# ensure our redirection url matches a pattern
def assert_redirect_url_match(pattern=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> was not found in the location: <?>", pattern, @response.redirect_url)
assert_block(msg) { @response.redirect_url_match?(pattern) }
end
deprecate :assert_redirect_url_match => "use assert(@response.redirect_url_match?(pattern))"
# -- template assertions ------------------------------------------------
# ensure that a template object with the given name exists
def assert_template_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not a template object", key )
assert_block(msg) { @response.has_template_object?(key) }
end
deprecate :assert_template_has => "use assert(@response.has_template_object?(key))"
# ensure that a template object with the given name does not exist
def assert_template_has_no(key=nil,message=nil) #:nodoc:
msg = build_message(message, "<?> is a template object <?>", key, @response.template_objects[key])
assert_block(msg) { !@response.has_template_object?(key) }
end
deprecate :assert_template_has_no => "use assert(!@response.has_template_object?(key))"
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
def assert_template_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, @response.template.assigns[key.to_s])
assert_block(msg) { expected == @response.template.assigns[key.to_s] }
end
alias_method :assert_assigned_equal, :assert_template_equal
deprecate :assert_assigned_equal => "use assert_equal(expected, @response.template.assigns[key.to_s])"
deprecate :assert_template_equal => "use assert_equal(expected, @response.template.assigns[key.to_s])"
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
# This will only work if the template rendered a valid XML document.
def assert_template_xpath_match(expression=nil, expected=nil, message=nil) #:nodoc:
xml, matches = REXML::Document.new(@response.body), []
xml.elements.each(expression) { |e| matches << e.text }
if matches.empty? then
msg = build_message(message, "<?> not found in document", expression)
flunk(msg)
return
elsif matches.length < 2 then
matches = matches.first
end
msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
assert_block(msg) { matches == expected }
end
deprecate :assert_template_xpath_match => "you should use assert_tag, instead"
# Assert the template object with the given name is an Active Record descendant and is valid.
def assert_valid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
assert_block(msg) { record.valid? }
end
deprecate :assert_valid_record => "use assert(assigns(key).valid?)"
# Assert the template object with the given name is an Active Record descendant and is invalid.
def assert_invalid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is valid)")
assert_block(msg) { !record.valid? }
end
deprecate :assert_invalid_record => "use assert(!assigns(key).valid?)"
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
def assert_valid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| !record.errors.invalid?(col) }
msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
deprecate :assert_valid_column_on_record => "use assert(!record.errors.invalid?(column)) instead"
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
def assert_invalid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| record.errors.invalid?(col) }
msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
deprecate :assert_invalid_column_on_record => "use assert(record.errors.invalid?(column)) instead"
private
def glue_columns(columns)
cols = []
cols << columns if columns.class == String
cols += columns if columns.class == Array
cols
end
def find_record_in_template(key = nil)
assert_not_nil assigns(key)
record = @response.template_objects[key]
assert_not_nil(record)
assert_kind_of ActiveRecord::Base, record
return record
end
end
end
end

View file

@ -2,24 +2,38 @@ module ActionController
module Assertions
module DomAssertions
# Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
def assert_dom_equal(expected, actual, message="")
#
# ==== 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
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+.
def assert_dom_not_equal(expected, actual, message="")
#
# ==== 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
end

View file

@ -2,6 +2,13 @@ module ActionController
module Assertions
module ModelAssertions
# Ensures that the passed record is valid by ActiveRecord 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)
clean_backtrace do
assert record.valid?, record.errors.full_messages.join("\n")
@ -9,4 +16,4 @@ module ActionController
end
end
end
end
end

View file

@ -1,19 +1,29 @@
require 'rexml/document'
require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document"
require 'html/document'
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
# * <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 ActionController::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}?")
@ -28,9 +38,18 @@ module ActionController
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.
# 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
#
def assert_redirected_to(options = {}, message=nil)
clean_backtrace do
assert_response(:redirect, message)
@ -79,10 +98,8 @@ module ActionController
url[key] = value
end
@response_diff = url[:expected].diff(url[:actual]) if url[:actual]
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>), difference: <?>",
url[:actual], @response_diff)
@response_diff = url[:actual].diff(url[:expected]) if url[:actual]
msg = build_message(message, "expected a redirect to <?>, found one to <?>, a difference of <?> ", url[:expected], url[:actual], @response_diff)
assert_block(msg) do
url[:expected].keys.all? do |k|
@ -106,6 +123,12 @@ module ActionController
end
# Asserts that the request was rendered with the appropriate template file.
#
# ==== Examples
#
# # assert that the "new" view template was rendered
# assert_template "new"
#
def assert_template(expected = nil, message=nil)
clean_backtrace do
rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file

View file

@ -1,25 +1,41 @@
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 match.
# 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+.
#
# assert_recognizes({:controller => 'items', :action => 'index'}, 'items') # check the default action
# assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list') # check a specific action
# assert_recognizes({:controller => 'items', :action => 'list', :id => '1'}, 'items/list/1') # check an action with a parameter
#
# Pass a hash in the second argument to specify the request method. This is useful for routes
# 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
# 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]
@ -43,12 +59,24 @@ module ActionController
end
end
# Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes.
# For example:
# 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] == ?/
@ -67,9 +95,25 @@ module ActionController
end
end
# Asserts that path and options match both ways; in other words, the URL generated from
# options is the same as path, and also that the options recognized from path are the same as options. This
# essentially combines assert_recognizes and assert_generates into one step.
# 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"}
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)

View file

@ -4,7 +4,7 @@
#++
require 'rexml/document'
require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document"
require 'html/document'
module ActionController
module Assertions
@ -13,15 +13,13 @@ module ActionController
end
# Adds the #assert_select method for use in Rails functional
# test cases.
#
# Use #assert_select to make assertions on the response HTML of a controller
# 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.
@ -29,7 +27,7 @@ module ActionController
# for example for dealing with feed item descriptions.
# * #assert_select_email -- Assertions on the HTML body of an e-mail.
#
# Also see HTML::Selector for learning how to use selectors.
# Also see HTML::Selector to learn how to use selectors.
module SelectorAssertions
# :call-seq:
# css_select(selector) => array
@ -49,12 +47,26 @@ module ActionController
# The selector may be a CSS selector expression (+String+), an expression
# with substitution values (+Array+) or an HTML::Selector object.
#
# For example:
# ==== 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
@ -66,6 +78,7 @@ module ActionController
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|
@ -105,12 +118,13 @@ module ActionController
# response HTML. Calling #assert_select inside an #assert_select block will
# run the assertion for each element selected by the enclosing assertion.
#
# For example:
# ==== Example
# assert_select "ol>li" do |elements|
# elements.each do |element|
# assert_select element, "li"
# end
# end
#
# Or for short:
# assert_select "ol>li" do
# assert_select "li"
@ -148,7 +162,7 @@ module ActionController
# 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
# ==== Examples
#
# # At least one form element
# assert_select "form"
@ -196,7 +210,7 @@ module ActionController
# 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.
@ -209,7 +223,7 @@ module ActionController
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
@ -277,14 +291,10 @@ module ActionController
# found one but expecting two.
message ||= content_mismatch if matches.empty?
# Test minimum/maximum occurrence.
if equals[:minimum]
assert matches.size >= equals[:minimum], message ||
"Expected at least #{equals[:minimum]} elements, found #{matches.size}."
end
if equals[:maximum]
assert matches.size <= equals[:maximum], message ||
"Expected at most #{equals[:maximum]} elements, found #{matches.size}."
end
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.
@ -300,7 +310,19 @@ module ActionController
# 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| ... }
@ -317,12 +339,17 @@ module ActionController
# 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 +:replace+, +:replace_html+ and
# +:insert_html+.
# 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 +:insert+ followed by an insertion position to narrow
# 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 +:top+, +:bottom+, +:before+ and +:after+.
# 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
#
@ -339,7 +366,7 @@ module ActionController
# but without distinguishing whether the content is returned in an HTML
# or JavaScript.
#
# === Examples
# ==== Examples
#
# # Replacing the element foo.
# # page.replace 'foo', ...
@ -352,6 +379,9 @@ module ActionController
# # 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""
@ -373,6 +403,7 @@ module ActionController
# any RJS statement.
if arg.is_a?(Symbol)
rjs_type = arg
if rjs_type == :insert
arg = args.shift
insertion = "insert_#{arg}".to_sym
@ -400,20 +431,29 @@ module ActionController
case rjs_type
when :chained_replace, :chained_replace_html
Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
when :remove, :show, :hide, :toggle
Regexp.new("#{statement}\\(\"#{id}\"\\)")
else
Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE)
end
# Duplicate the body since the next step involves destroying it.
matches = nil
@response.body.gsub(pattern) do |match|
html = unescape_rjs($2)
matches ||= []
matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? }
""
case rjs_type
when :remove, :show, :hide, :toggle
matches = @response.body.match(pattern)
else
@response.body.gsub(pattern) do |match|
html = unescape_rjs($2)
matches ||= []
matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? }
""
end
end
if matches
if block_given?
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
@ -441,8 +481,20 @@ module ActionController
# 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.
#
# === Example
# ==== 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
@ -493,11 +545,19 @@ module ActionController
# You must enable deliveries for this assertion to work, use:
# ActionMailer::Base.perform_deliveries = true
#
# === Example
# ==== 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
#
# assert_select_email do
# assert_select "h1", "Email alert"
# end
def assert_select_email(&block)
deliveries = ActionMailer::Base.deliveries
assert !deliveries.empty?, "No e-mail in delivery list"
@ -519,6 +579,10 @@ module ActionController
:replace_html => /Element\.update/,
:chained_replace => /\.replace/,
:chained_replace_html => /\.update/,
:remove => /Element\.remove/,
:show => /Element\.show/,
:hide => /Element\.hide/,
:toggle => /Element\.toggle/
}
RJS_INSERTIONS = [:top, :bottom, :before, :after]
RJS_INSERTIONS.each do |insertion|
@ -537,10 +601,12 @@ module ActionController
# #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.headers["Content-Type"]
content_type = @response.content_type
if content_type && content_type =~ /text\/javascript/
body = @response.body.dup
root = HTML::Node.new(nil)
while true
next if body.sub!(RJS_PATTERN_EVERYTHING) do |match|
html = unescape_rjs($3)
@ -550,6 +616,7 @@ module ActionController
end
break
end
root
else
html_document.root
@ -560,6 +627,7 @@ module ActionController
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', '<')
@ -567,7 +635,6 @@ module ActionController
unescaped.gsub!(RJS_PATTERN_UNICODE_ESCAPED_CHAR) {|u| [$1.hex].pack('U*')}
unescaped
end
end
end
end

View file

@ -1,8 +1,9 @@
require 'rexml/document'
require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document"
require 'html/document'
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
@ -37,8 +38,8 @@ module ActionController
# 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.
# given value. This will not match HTML tags in the body of a
# tag--only text.
#
# Conditions are matched using the following algorithm:
#
@ -48,39 +49,39 @@ module ActionController
# * if the condition is +true+, the value must not be +nil+.
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
# Usage:
# === Examples
#
# # assert that there is a "span" tag
# # Assert that there is a "span" tag
# assert_tag :tag => "span"
#
# # assert that there is a "span" tag with id="x"
# # 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 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 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 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 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 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)
# # 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
# # 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
# # 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",
@ -90,7 +91,7 @@ module ActionController
# :descendant => { :tag => "span",
# :child => /hello world/ }
#
# <strong>Please note</strong: #assert_tag and #assert_no_tag only work
# <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
@ -105,6 +106,18 @@ module ActionController
# 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

View file

@ -11,10 +11,16 @@ require 'set'
module ActionController #:nodoc:
class ActionControllerError < StandardError #:nodoc:
end
class SessionRestoreError < ActionControllerError #:nodoc:
end
class MissingTemplate < ActionControllerError #:nodoc:
end
class RenderError < ActionControllerError #:nodoc:
end
class RoutingError < ActionControllerError #:nodoc:
attr_reader :failures
def initialize(message, failures=[])
@ -22,14 +28,39 @@ module ActionController #:nodoc:
@failures = failures
end
end
class MethodNotAllowed < ActionControllerError #:nodoc:
attr_reader :allowed_methods
def initialize(*allowed_methods)
super("Only #{allowed_methods.to_sentence} requests are allowed.")
@allowed_methods = allowed_methods
end
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:
end
class UnknownController < ActionControllerError #:nodoc:
end
class UnknownAction < ActionControllerError #:nodoc:
end
class MissingFile < ActionControllerError #:nodoc:
end
class RenderError < ActionControllerError #:nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc:
DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
@ -37,13 +68,15 @@ module ActionController #:nodoc:
super(message || DEFAULT_MESSAGE)
end
end
class DoubleRenderError < ActionControllerError #:nodoc:
DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and only 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\". Finally, note that to cause a before filter to halt execution of the rest of the filter chain, the filter must return false, explicitly, so \"render(...) and return false\"."
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 RedirectBackError < ActionControllerError #: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"].'
@ -52,6 +85,9 @@ module ActionController #:nodoc:
end
end
class UnknownHttpMethod < ActionControllerError #:nodoc:
end
# Action Controllers are the core of a web request in Rails. They are made up of one or more actions that are executed
# on request and then either render a template or redirect to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through Rails Routes.
@ -71,7 +107,7 @@ module ActionController #:nodoc:
#
# Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
# after executing code in the action. For example, the +index+ action of the +GuestBookController+ would render the
# template <tt>app/views/guestbook/index.rhtml</tt> by default after populating the <tt>@entries</tt> instance variable.
# template <tt>app/views/guestbook/index.erb</tt> by default after populating the <tt>@entries</tt> instance variable.
#
# Unlike index, the sign action will not render a template. After performing its main purpose (creating a
# new entry in the guest book), it initiates a redirect instead. This redirect works by returning an external
@ -128,17 +164,26 @@ module ActionController #:nodoc:
# For removing objects from the session, you can either assign a single key to nil, like <tt>session[:person] = nil</tt>, or you can
# remove the entire session with reset_session.
#
# By default, sessions are stored on the file system in <tt>RAILS_ROOT/tmp/sessions</tt>. Any object can be placed in the session
# (as long as it can be Marshalled). But remember that 1000 active sessions each storing a 50kb object could lead to a 50MB store on the filesystem.
# In other words, think carefully about size and caching before resorting to the use of the session on the filesystem.
# Sessions are stored in a browser cookie that's cryptographically signed, but unencrypted, by default. This prevents
# the user from tampering with the session but also allows him to see its contents.
#
# An alternative to storing sessions on disk is to use ActiveRecordStore to store sessions in your database, which can solve problems
# caused by storing sessions in the file system and may speed up your application. To use ActiveRecordStore, uncomment the line:
# Do not put secret information in session!
#
# Other options for session storage are:
#
# ActiveRecordStore: sessions are stored in your database, which works better than PStore with multiple app servers and,
# unlike CookieStore, hides your session contents from the user. To use ActiveRecordStore, set
#
# config.action_controller.session_store = :active_record_store
#
# in your <tt>environment.rb</tt> and run <tt>rake db:sessions:create</tt>.
#
# MemCacheStore: sessions are stored as entries in your memcached cache. Set the session store type in <tt>environment.rb</tt>:
#
# config.action_controller.session_store = :mem_cache_store
#
# This assumes that memcached has been installed and configured properly. See the MemCacheStore docs for more information.
#
# == Responses
#
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
@ -192,7 +237,7 @@ module ActionController #:nodoc:
#
# == Calling multiple redirects or renders
#
# An action should conclude with a single render or redirect. Attempting to try to do either again will result in a DoubleRenderError:
# An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError:
#
# def do_something
# redirect_to :action => "elsewhere"
@ -209,7 +254,6 @@ module ActionController #:nodoc:
class Base
DEFAULT_RENDER_STATUS_CODE = "200 OK"
include Reloadable::Deprecated
include StatusCodes
# Determines whether the view has access to controller internals @request, @response, @session, and @template.
@ -249,7 +293,7 @@ module ActionController #:nodoc:
# The param_parsers hash lets you register handlers which will process the http body and add parameters to the
# <tt>params</tt> hash. These handlers are invoked for post and put requests.
#
# By default application/xml is enabled. A XmlSimple class with the same param name as the root will be instanciated
# By default application/xml is enabled. A XmlSimple class with the same param name as the root will be instantiated
# in the <tt>params</tt>. This allows XML requests to mask themselves as regular form submissions, so you can have one
# action serve both regular forms and web service requests.
#
@ -271,17 +315,15 @@ module ActionController #:nodoc:
# A YAML parser is also available and can be turned on with:
#
# ActionController::Base.param_parsers[Mime::YAML] = :yaml
@@param_parsers = { Mime::XML => :xml_simple }
@@param_parsers = { Mime::MULTIPART_FORM => :multipart_form,
Mime::URL_ENCODED_FORM => :url_encoded_form,
Mime::XML => :xml_simple }
cattr_accessor :param_parsers
# Controls the default charset for all renders.
@@default_charset = "utf-8"
cattr_accessor :default_charset
# Template root determines the base from which template references will be made. So a call to render("test/template")
# will be converted to "#{template_root}/test/template.rhtml".
class_inheritable_accessor :template_root
# The logger is used for generating information on the action run-time (including benchmarking) if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
cattr_accessor :logger
@ -295,6 +337,18 @@ module ActionController #:nodoc:
# Controls the resource action separator
@@resource_action_separator = "/"
cattr_accessor :resource_action_separator
# Sets the token parameter name for RequestForgery. Calling #protect_from_forgery sets it to :authenticity_token by default
cattr_accessor :request_forgery_protection_token
# Indicates whether or not optimise the generated named
# route helper methods
cattr_accessor :optimise_named_routes
self.optimise_named_routes = true
# 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
# Holds the request object that's primarily used to get environment variables through access like
# <tt>request.env["REQUEST_URI"]</tt>.
@ -353,19 +407,57 @@ module ActionController #:nodoc:
# By default, all methods defined in ActionController::Base and included modules are hidden.
# More methods can be hidden using <tt>hide_actions</tt>.
def hidden_actions
write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods) unless read_inheritable_attribute(:hidden_actions)
unless read_inheritable_attribute(:hidden_actions)
write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods.map(&:to_s))
end
read_inheritable_attribute(:hidden_actions)
end
# Hide each of the given methods from being callable as actions.
def hide_action(*names)
write_inheritable_attribute(:hidden_actions, hidden_actions | names.collect { |n| n.to_s })
write_inheritable_attribute(:hidden_actions, hidden_actions | names.map(&:to_s))
end
# Replace sensitive paramater data from the request log.
# Filters paramaters that have any of the arguments as a substring.
## View load paths determine the bases from which template references can be made. So a call to
## render("test/template") will be looked up in the view load paths array and the closest match will be
## returned.
def view_paths
@view_paths || superclass.view_paths
end
def view_paths=(value)
@view_paths = value
end
# Adds a view_path to the front of the view_paths array.
# If the current class has no view paths, copy them from
# the superclass. This change will be visible for all future requests.
#
# ArticleController.prepend_view_path("views/default")
# ArticleController.prepend_view_path(["views/default", "views/custom"])
#
def prepend_view_path(path)
@view_paths = superclass.view_paths.dup if @view_paths.nil?
view_paths.unshift(*path)
end
# Adds a view_path to the end of the view_paths array.
# If the current class has no view paths, copy them from
# the superclass. This change will be visible for all future requests.
#
# ArticleController.append_view_path("views/default")
# ArticleController.append_view_path(["views/default", "views/custom"])
#
def append_view_path(path)
@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 paramater hash and all
# 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.
#
@ -412,13 +504,10 @@ module ActionController #:nodoc:
# Don't render layouts for templates with the given extensions.
def exempt_from_layout(*extensions)
@@exempt_from_layout.merge extensions.collect { |extension|
if extension.is_a?(Regexp)
extension
else
/\.#{Regexp.escape(extension.to_s)}$/
end
}
regexps = extensions.collect do |extension|
extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/
end
@@exempt_from_layout.merge regexps
end
end
@ -435,6 +524,9 @@ module ActionController #:nodoc:
send(method, *arguments)
assign_default_content_type_and_charset
response.request = request
response.prepare! unless component_request?
response
ensure
process_cleanup
@ -452,18 +544,25 @@ module ActionController #:nodoc:
# * <tt>:only_path</tt> -- if true, returns the relative URL (omitting the protocol, host name, and port) (<tt>false</tt> by default)
# * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
# is currently not recommended since it breaks caching.
# * <tt>:host</tt> -- overrides the default (current) host if provided
# * <tt>:protocol</tt> -- overrides the default (current) protocol if provided
# * <tt>:host</tt> -- overrides the default (current) host if provided.
# * <tt>:protocol</tt> -- overrides the default (current) protocol if provided.
# * <tt>:port</tt> -- optionally specify the port to connect to.
# * <tt>:user</tt> -- Inline HTTP authentication (only plucked out if :password is also present).
# * <tt>:password</tt> -- Inline HTTP authentication (only plucked out if :user is also present).
# * <tt>:skip_relative_url_root</tt> -- if true, the url is not constructed using the relative_url_root of the request so the path
# will include the web server relative installation directory.
#
# The URL is generated from the remaining keys in the hash. A URL contains two key parts: the <base> and a query string.
# Routes composes a query string as the key/value pairs not included in the <base>.
#
# The default Routes setup supports a typical Rails path of "controller/action/id" where action and id are optional, with
# action defaulting to 'index' when not given. Here are some typical url_for statements and their corresponding URLs:
#  
# url_for :controller => 'posts', :action => 'recent' # => 'proto://host.com/posts/recent'
# url_for :controller => 'posts', :action => 'index' # => 'proto://host.com/posts'
# url_for :controller => 'posts', :action => 'show', :id => 10 # => 'proto://host.com/posts/show/10'
#
# url_for :controller => 'posts', :action => 'recent' # => 'proto://host.com/posts/recent'
# url_for :controller => 'posts', :action => 'index' # => 'proto://host.com/posts'
# url_for :controller => 'posts', :action => 'index', :port=>'8033' # => 'proto://host.com:8033/posts'
# url_for :controller => 'posts', :action => 'show', :id => 10 # => 'proto://host.com/posts/show/10'
# url_for :controller => 'posts', :user => 'd', :password => '123' # => 'proto://d:123@host.com/posts'
#
# When generating a new URL, missing values may be filled in from the current request's parameters. For example,
# <tt>url_for :action => 'some_action'</tt> will retain the current controller, as expected. This behavior extends to
@ -491,9 +590,9 @@ module ActionController #:nodoc:
# However, you might ask why the action from the current request, 'contacts', isn't carried over into the new URL. The
# answer has to do with the order in which the parameters appear in the generated path. In a nutshell, since the
# value that appears in the slot for <tt>:first</tt> is not equal to default value for <tt>:first</tt> we stop using
# defaults. On it's own, this rule can account for much of the typical Rails URL behavior.
# defaults. On its own, this rule can account for much of the typical Rails URL behavior.
#  
# Although a convienence, defaults can occasionaly get in your way. In some cases a default persists longer than desired.
# Although a convenience, defaults can occasionally get in your way. In some cases a default persists longer than desired.
# The default may be cleared by adding <tt>:name => nil</tt> to <tt>url_for</tt>'s options.
# This is often required when writing form helpers, since the defaults in play may vary greatly depending upon where the
# helper is used from. The following line will redirect to PostController's default action, regardless of the page it is
@ -509,22 +608,14 @@ module ActionController #:nodoc:
#
# This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt>
# would have slashed-off the path components after the changed action.
def url_for(options = {}, *parameters_for_method_reference) #:doc:
case options
def url_for(options = nil) #:doc:
case options || {}
when String
options
when Symbol
ActiveSupport::Deprecation.warn(
"You called url_for(:#{options}), which is a deprecated API call. Instead you should use the named " +
"route directly, like #{options}(). Using symbols and parameters with url_for will be removed from Rails 2.0.",
caller
)
send(options, *parameters_for_method_reference)
when Hash
@url.rewrite(rewrite_options(options))
else
polymorphic_url(options)
end
end
@ -543,11 +634,41 @@ module ActionController #:nodoc:
self.class.controller_path
end
# Test whether the session is enabled for this request.
def session_enabled?
request.session_options && request.session_options[:disabled] != false
end
self.view_paths = []
# View load paths for controller.
def view_paths
(@template || self.class).view_paths
end
def view_paths=(value)
(@template || self.class).view_paths = value
end
# Adds a view_path to the front of the view_paths array.
# This change affects the current request only.
#
# self.prepend_view_path("views/default")
# self.prepend_view_path(["views/default", "views/custom"])
#
def prepend_view_path(path)
(@template || self.class).prepend_view_path(path)
end
# Adds a view_path to the end of the view_paths array.
# This change affects the current request only.
#
# self.append_view_path("views/default")
# self.append_view_path(["views/default", "views/custom"])
#
def append_view_path(path)
(@template || self.class).append_view_path(path)
end
protected
# Renders the content that will be returned to the browser as the response body.
#
@ -567,10 +688,6 @@ module ActionController #:nodoc:
# # but with a custom layout
# render :action => "long_goal", :layout => "spectacular"
#
# _Deprecation_ _notice_: This used to have the signatures <tt>render_action("action", status = 200)</tt>,
# <tt>render_without_layout("controller/action", status = 200)</tt>, and
# <tt>render_with_layout("controller/action", status = 200, layout)</tt>.
#
# === Rendering partials
#
# Partial rendering in a controller is most commonly used together with Ajax calls that only update one or a few elements on a page
@ -581,6 +698,10 @@ module ActionController #:nodoc:
# # Renders the same partial with a local variable.
# render :partial => "person", :locals => { :name => "david" }
#
# # Renders the partial, making @new_person available through
# # the local variable 'person'
# render :partial => "person", :object => @new_person
#
# # Renders a collection of the same partial by making each element
# # of @winners available through the local variable "person" as it
# # builds the complete response.
@ -602,16 +723,19 @@ module ActionController #:nodoc:
# Note that the partial filename must also be a valid Ruby variable name,
# so e.g. 2005 and register-user are invalid.
#
# _Deprecation_ _notice_: This used to have the signatures
# <tt>render_partial(partial_path = default_template_name, object = nil, local_assigns = {})</tt> and
# <tt>render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})</tt>.
#
# == Automatic etagging
#
# Rendering will automatically insert the etag header on 200 OK responses. The etag is calculated using MD5 of the
# response body. If a request comes in that has a matching etag, the response will be changed to a 304 Not Modified
# and the response body will be set to an empty string. No etag header will be inserted if it's already set.
#
# === Rendering a template
#
# Template rendering works just like action rendering except that it takes a path relative to the template root.
# The current layout is automatically applied.
#
# # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.rhtml)
# # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.erb)
# render :template => "weblog/show"
#
# === Rendering a file
@ -620,18 +744,16 @@ module ActionController #:nodoc:
# is assumed to be absolute, and the current layout is not applied.
#
# # Renders the template located at the absolute filesystem path
# render :file => "/path/to/some/template.rhtml"
# render :file => "c:/path/to/some/template.rhtml"
# render :file => "/path/to/some/template.erb"
# render :file => "c:/path/to/some/template.erb"
#
# # Renders a template within the current layout, and with a 404 status code
# render :file => "/path/to/some/template.rhtml", :layout => true, :status => 404
# render :file => "c:/path/to/some/template.rhtml", :layout => true, :status => 404
# render :file => "/path/to/some/template.erb", :layout => true, :status => 404
# render :file => "c:/path/to/some/template.erb", :layout => true, :status => 404
#
# # Renders a template relative to the template root and chooses the proper file extension
# render :file => "some/template", :use_full_path => true
#
# _Deprecation_ _notice_: This used to have the signature <tt>render_file(path, status = 200)</tt>
#
# === Rendering text
#
# Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text
@ -644,11 +766,11 @@ module ActionController #:nodoc:
# render :text => "Explosion!", :status => 500
#
# # Renders the clear text "Hi there!" within the current active layout (if one exists)
# render :text => "Explosion!", :layout => true
# render :text => "Hi there!", :layout => true
#
# # Renders the clear text "Hi there!" within the layout
# # placed in "app/views/layouts/special.r(html|xml)"
# render :text => "Explosion!", :layout => "special"
# render :text => "Hi there!", :layout => "special"
#
# The :text option can also accept a Proc object, which can be used to manually control the page generation. This should
# generally be avoided, as it violates the separation between code and content, and because almost everything that can be
@ -657,20 +779,24 @@ module ActionController #:nodoc:
# # Renders "Hello from code!"
# render :text => proc { |response, output| output.write("Hello from code!") }
#
# _Deprecation_ _notice_: This used to have the signature <tt>render_text("text", status = 200)</tt>
#
# === Rendering JSON
#
# Rendering JSON sets the content type to text/x-json and optionally wraps the JSON in a callback. It is expected
# that the response will be eval'd for use as a data structure.
# Rendering JSON sets the content type to application/json and optionally wraps the JSON in a callback. It is expected
# that the response will be parsed (or eval'd) for use as a data structure.
#
# # Renders '{name: "David"}'
# # Renders '{"name": "David"}'
# render :json => {:name => "David"}.to_json
#
# Sometimes the result isn't handled directly by a script (such as when the request comes from a SCRIPT tag),
# so the callback option is provided for these cases.
# It's not necessary to call <tt>to_json</tt> on the object you want to render, since <tt>render</tt> will
# automatically do that for you:
#
# # Renders 'show({name: "David"})'
# # Also renders '{"name": "David"}'
# render :json => {:name => "David"}
#
# Sometimes the result isn't handled directly by a script (such as when the request comes from a SCRIPT tag),
# so the <tt>:callback</tt> option is provided for these cases.
#
# # Renders 'show({"name": "David"})'
# render :json => {:name => "David"}.to_json, :callback => 'show'
#
# === Rendering an inline template
@ -683,13 +809,11 @@ module ActionController #:nodoc:
# render :inline => "<%= 'hello, ' * 3 + 'again' %>"
#
# # Renders "<p>Good seeing you!</p>" using Builder
# render :inline => "xml.p { 'Good seeing you!' }", :type => :rxml
# render :inline => "xml.p { 'Good seeing you!' }", :type => :builder
#
# # Renders "hello david"
# render :inline => "<%= 'hello ' + name %>", :locals => { :name => "david" }
#
# _Deprecation_ _notice_: This used to have the signature <tt>render_template(template, status = 200, type = :rhtml)</tt>
#
# === Rendering inline JavaScriptGenerator page updates
#
# In addition to rendering JavaScriptGenerator page updates with Ajax in RJS templates (see ActionView::Base for details),
@ -700,35 +824,21 @@ module ActionController #:nodoc:
# page.visual_effect :highlight, 'user_list'
# end
#
# === Rendering nothing
# === Rendering with status and location headers
#
# Rendering nothing is often convenient in combination with Ajax calls that perform their effect client-side or
# when you just want to communicate a status code. Due to a bug in Safari, nothing actually means a single space.
# All renders take the :status and :location options and turn them into headers. They can even be used together:
#
# # Renders an empty response with status code 200
# render :nothing => true
#
# # Renders an empty response with status code 401 (access denied)
# render :nothing => true, :status => 401
def render(options = nil, deprecated_status = nil, &block) #:doc:
# render :xml => post.to_xml, :status => :created, :location => post_url(post)
def render(options = nil, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed?
if options.nil?
return render_file(default_template_name, deprecated_status, true)
return render_for_file(default_template_name, nil, true)
else
# Backwards compatibility
unless options.is_a?(Hash)
if options == :update
options = { :update => true }
else
ActiveSupport::Deprecation.warn(
"You called render('#{options}'), which is a deprecated API call. Instead you use " +
"render :file => #{options}. Calling render with just a string will be removed from Rails 2.0.",
caller
)
return render_file(options, deprecated_status, true)
end
if options == :update
options = { :update => true }
elsif !options.is_a?(Hash)
raise RenderError, "You called render with invalid options : #{options}"
end
end
@ -736,52 +846,72 @@ module ActionController #:nodoc:
response.content_type = content_type.to_s
end
if location = options[:location]
response.headers["Location"] = url_for(location)
end
if text = options[:text]
render_text(text, options[:status])
render_for_text(text, options[:status])
else
if file = options[:file]
render_file(file, options[:status], options[:use_full_path], options[:locals] || {})
render_for_file(file, options[:status], options[:use_full_path], options[:locals] || {})
elsif template = options[:template]
render_file(template, options[:status], true)
render_for_file(template, options[:status], true)
elsif inline = options[:inline]
render_template(inline, options[:status], options[:type], options[:locals] || {})
add_variables_to_assigns
render_for_text(@template.render_template(options[:type], inline, nil, options[:locals] || {}), options[:status])
elsif action_name = options[:action]
ActiveSupport::Deprecation.silence do
render_action(action_name, options[:status], options[:layout])
end
template = default_template_name(action_name.to_s)
if options[:layout] && !template_exempt_from_layout?(template)
render_with_a_layout(:file => template, :status => options[:status], :use_full_path => true, :layout => true)
else
render_with_no_layout(:file => template, :status => options[:status], :use_full_path => true)
end
elsif xml = options[:xml]
render_xml(xml, options[:status])
response.content_type ||= Mime::XML
render_for_text(xml.respond_to?(:to_xml) ? xml.to_xml : xml, options[:status])
elsif json = options[:json]
render_json(json, options[:callback], options[:status])
json = json.to_json unless json.is_a?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
response.content_type ||= Mime::JSON
render_for_text(json, options[:status])
elsif partial = options[:partial]
partial = default_template_name if partial == true
add_variables_to_assigns
if collection = options[:collection]
render_partial_collection(partial, collection, options[:spacer_template], options[:locals], options[:status])
render_for_text(
@template.send!(:render_partial_collection, partial, collection,
options[:spacer_template], options[:locals]), options[:status]
)
else
render_partial(partial, ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals], options[:status])
render_for_text(
@template.send!(:render_partial, partial,
ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]), options[:status]
)
end
elsif options[:update]
add_variables_to_assigns
@template.send :evaluate_assigns
@template.send! :evaluate_assigns
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(@template, &block)
render_javascript(generator.to_s)
response.content_type = Mime::JS
render_for_text(generator.to_s)
elsif options[:nothing]
# Safari doesn't pass the headers of the return if the response is zero length
render_text(" ", options[:status])
render_for_text(" ", options[:status])
else
render_file(default_template_name, options[:status], true)
render_for_file(default_template_name, options[:status], true)
end
end
end
@ -789,87 +919,13 @@ module ActionController #:nodoc:
# Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead
# of sending it as the response body to the browser.
def render_to_string(options = nil, &block) #:doc:
ActiveSupport::Deprecation.silence { render(options, &block) }
render(options, &block)
ensure
erase_render_results
forget_variables_added_to_assigns
reset_variables_added_to_assigns
end
def render_action(action_name, status = nil, with_layout = true) #:nodoc:
template = default_template_name(action_name.to_s)
if with_layout && !template_exempt_from_layout?(template)
render_with_layout(:file => template, :status => status, :use_full_path => true, :layout => true)
else
render_without_layout(:file => template, :status => status, :use_full_path => true)
end
end
def render_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc:
add_variables_to_assigns
assert_existence_of_template_file(template_path) if use_full_path
logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
render_text(@template.render_file(template_path, use_full_path, locals), status)
end
def render_template(template, status = nil, type = :rhtml, local_assigns = {}) #:nodoc:
add_variables_to_assigns
render_text(@template.render_template(type, template, nil, local_assigns), status)
end
def render_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true
response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
response.body ||= ''
response.body << text
else
response.body = text
end
end
def render_javascript(javascript, status = nil, append_response = true) #:nodoc:
response.content_type = Mime::JS
render_text(javascript, status, append_response)
end
def render_xml(xml, status = nil) #:nodoc:
response.content_type = Mime::XML
render_text(xml, status)
end
def render_json(json, callback = nil, status = nil) #:nodoc:
json = "#{callback}(#{json})" unless callback.blank?
response.content_type = Mime::JSON
render_text(json, status)
end
def render_nothing(status = nil) #:nodoc:
render_text(' ', status)
end
def render_partial(partial_path = default_template_name, object = nil, local_assigns = nil, status = nil) #:nodoc:
add_variables_to_assigns
render_text(@template.render_partial(partial_path, object, local_assigns), status)
end
def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil, status = nil) #:nodoc:
add_variables_to_assigns
render_text(@template.render_partial_collection(partial_name, collection, partial_spacer_template, local_assigns), status)
end
def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
render_with_a_layout(template_name, status, layout)
end
def render_without_layout(template_name = default_template_name, status = nil) #:nodoc:
render_with_no_layout(template_name, status)
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
@ -887,19 +943,9 @@ module ActionController #:nodoc:
raise ArgumentError, "too many arguments to head"
elsif args.empty?
raise ArgumentError, "too few arguments to head"
elsif args.length == 2
status = args.shift
options = args.shift
elsif args.first.is_a?(Hash)
options = args.first
else
status = args.first
options = {}
end
raise ArgumentError, "head requires an options hash" if !options.is_a?(Hash)
status = interpret_status(status || options.delete(:status) || :ok)
options = args.extract_options!
status = interpret_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
@ -956,47 +1002,62 @@ module ActionController #:nodoc:
# 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>String starting with protocol:// (like http://)</tt>: Is passed straight through as the target for redirection.
# * <tt>String not containing a protocol</tt>: 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.
# * <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 starting with protocol:// (like http://)</tt> - Is passed straight through as the target for redirection.
# * <tt>String not containing a protocol</tt> - 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 redirect_to(request.env["HTTP_REFERER"])
#
# 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.
# 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 rescueing RedirectBackError.
def redirect_to(options = {}, *parameters_for_method_reference) #:doc:
# behavior for this case by rescuing RedirectBackError.
def redirect_to(options = {}, response_status = {}) #:doc:
if options.is_a?(Hash) && options[:status]
status = options.delete(:status)
elsif response_status[:status]
status = response_status[:status]
else
status = 302
end
case options
when %r{^\w+://.*}
raise DoubleRenderError if performed?
logger.info("Redirected to #{options}") if logger
response.redirect(options)
logger.info("Redirected to #{options}") if logger && logger.info?
response.redirect(options, interpret_status(status))
response.redirected_to = options
@performed_redirect = true
when String
redirect_to(request.protocol + request.host_with_port + options)
redirect_to(request.protocol + request.host_with_port + options, :status=>status)
when :back
request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"]) : raise(RedirectBackError)
request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"], :status=>status) : raise(RedirectBackError)
when Hash
redirect_to(url_for(options), :status=>status)
response.redirected_to = options
else
if parameters_for_method_reference.empty?
redirect_to(url_for(options))
response.redirected_to = options
else
# TOOD: Deprecate me!
redirect_to(url_for(options, *parameters_for_method_reference))
response.redirected_to, response.redirected_to_method_params = options, parameters_for_method_reference
end
redirect_to(url_for(options), :status=>status)
end
end
@ -1030,23 +1091,35 @@ module ActionController #:nodoc:
response.session = @_session
end
private
def self.view_class
@view_class ||=
# create a new class based on the default template class and include helper methods
returning Class.new(ActionView::Base) do |view_class|
view_class.send(:include, master_helper_module)
end
def render_for_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc:
add_variables_to_assigns
assert_existence_of_template_file(template_path) if use_full_path
logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
render_for_text(@template.render_file(template_path, use_full_path, locals), status)
end
def self.view_root
@view_root ||= template_root
end
def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true
response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
response.body ||= ''
response.body << text.to_s
else
response.body = text.is_a?(Proc) ? text : text.to_s
end
end
def initialize_template_class(response)
raise "You must assign a template class through ActionController.template_class= before processing a request" unless @@template_class
unless @@template_class
raise "You must assign a template class through ActionController.template_class= before processing a request"
end
response.template = self.class.view_class.new(self.class.view_root, {}, self)
response.template = ActionView::Base.new(view_paths, {}, self)
response.template.extend self.class.master_helper_module
response.redirected_to = nil
@performed_render = @performed_redirect = false
end
@ -1062,26 +1135,6 @@ module ActionController #:nodoc:
@assigns = @_response.template.assigns
@_headers = @_response.headers
assign_deprecated_shortcuts(request, response)
end
# TODO: assigns cookies headers params request response template
DEPRECATED_INSTANCE_VARIABLES = %w(cookies flash headers params request response session)
# Gone after 1.2.
def assign_deprecated_shortcuts(request, response)
DEPRECATED_INSTANCE_VARIABLES.each do |method|
var = "@#{method}"
if instance_variables.include?(var)
value = instance_variable_get(var)
unless ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy === value
raise "Deprecating #{var}, but it's already set to #{value.inspect}! Use the #{method}= writer method instead of setting #{var} directly."
end
end
instance_variable_set var, ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, method)
end
end
def initialize_current_url
@ -1089,22 +1142,26 @@ module ActionController #:nodoc:
end
def log_processing
if logger
if logger && logger.info?
logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]"
logger.info " Session ID: #{@_session.session_id}" if @_session and @_session.respond_to?(:session_id)
logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(params).inspect : params.inspect}"
end
end
def default_render #:nodoc:
render
end
def perform_action
if self.class.action_methods.include?(action_name)
send(action_name)
render unless performed?
default_render unless performed?
elsif respond_to? :method_missing
send(:method_missing, action_name)
render unless performed?
method_missing action_name
default_render unless performed?
elsif template_exists? && template_public?
render
default_render
else
raise UnknownAction, "No action responded to #{action_name}", caller
end
@ -1132,7 +1189,7 @@ module ActionController #:nodoc:
end
def self.action_methods
@action_methods ||= Set.new(public_instance_methods - hidden_actions)
@action_methods ||= Set.new(public_instance_methods.map(&:to_s)) - hidden_actions
end
def add_variables_to_assigns
@ -1160,7 +1217,7 @@ module ActionController #:nodoc:
end
def add_class_variables_to_assigns
%w(template_root logger template_class ignore_missing_templates).each do |cvar|
%w(view_paths logger template_class ignore_missing_templates).each do |cvar|
@assigns[cvar] = self.send(cvar)
end
end
@ -1199,16 +1256,17 @@ module ActionController #:nodoc:
end
def template_exempt_from_layout?(template_name = default_template_name)
extension = @template.pick_template_extension(template_name) rescue nil
extension = @template && @template.pick_template_extension(template_name)
name_with_extension = !template_name.include?('.') && extension ? "#{template_name}.#{extension}" : template_name
extension == :rjs || @@exempt_from_layout.any? { |ext| name_with_extension =~ ext }
@@exempt_from_layout.any? { |ext| name_with_extension =~ ext }
end
def assert_existence_of_template_file(template_name)
unless template_exists?(template_name) || ignore_missing_templates
full_template_path = @template.send(:full_template_path, template_name, 'rhtml')
full_template_path = template_name.include?('.') ? template_name : "#{template_name}.#{@template.template_format}.erb"
display_paths = view_paths.join(':')
template_type = (template_name =~ /layouts/i) ? 'layout' : 'template'
raise(MissingTemplate, "Missing #{template_type} #{full_template_path}")
raise(MissingTemplate, "Missing #{template_type} #{full_template_path} in view path #{display_paths}")
end
end

View file

@ -40,50 +40,55 @@ module ActionController #:nodoc:
end
end
def render_with_benchmark(options = nil, deprecated_status = nil, &block)
unless logger
render_without_benchmark(options, deprecated_status, &block)
else
db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
protected
def render_with_benchmark(options = nil, deprecated_status = nil, &block)
unless logger
render_without_benchmark(options, &block)
else
db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
render_output = nil
@rendering_runtime = Benchmark::measure{ render_output = render_without_benchmark(options, deprecated_status, &block) }.real
render_output = nil
@rendering_runtime = Benchmark::measure{ render_output = render_without_benchmark(options, &block) }.real
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
@rendering_runtime -= @db_rt_after_render
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
@rendering_runtime -= @db_rt_after_render
end
render_output
end
end
render_output
end
end
def perform_action_with_benchmark
unless logger
perform_action_without_benchmark
else
runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if defined?(@rendering_runtime)
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message << " | #{headers["Status"]}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
end
end
private
def perform_action_with_benchmark
unless logger
perform_action_without_benchmark
else
runtime = [ Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001 ].max
log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if defined?(@rendering_runtime)
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message << " | #{headers["Status"]}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
response.headers["X-Runtime"] = sprintf("%.5f", runtime)
end
end
def rendering_runtime(runtime)
" | Rendering: #{sprintf("%.5f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime * 100) / runtime)}%)"
percentage = @rendering_runtime * 100 / runtime
" | Rendering: %.5f (%d%%)" % [@rendering_runtime, percentage.to_i]
end
def active_record_runtime(runtime)
db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime += @db_rt_before_render if @db_rt_before_render
db_runtime += @db_rt_after_render if @db_rt_after_render
db_percentage = (db_runtime * 100) / runtime
" | DB: #{sprintf("%.5f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
db_percentage = db_runtime * 100 / runtime
" | DB: %.5f (%d%%)" % [db_runtime, db_percentage.to_i]
end
end
end

View file

@ -11,9 +11,13 @@ module ActionController #:nodoc:
# Note: To turn off all caching and sweeping, set Base.perform_caching = false.
module Caching
def self.included(base) #:nodoc:
base.send(:include, Pages, Actions, Fragments, Sweeping)
base.class_eval do
include Pages, Actions, Fragments
if defined? ActiveRecord
include Sweeping, SqlCache
end
@@perform_caching = true
cattr_accessor :perform_caching
end
@ -77,7 +81,7 @@ module ActionController #:nodoc:
return unless perform_caching
benchmark "Expired page: #{page_cache_file(path)}" do
File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path))
File.delete(page_cache_path(path)) if File.exist?(page_cache_path(path))
end
end
@ -96,14 +100,13 @@ module ActionController #:nodoc:
# matches the triggering url.
def caches_page(*actions)
return unless perform_caching
actions.each do |action|
class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
end
actions = actions.map(&:to_s)
after_filter { |c| c.cache_page if actions.include?(c.action_name) }
end
private
def page_cache_file(path)
name = ((path.empty? || path == "/") ? "/index" : URI.unescape(path))
name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/'))
name << page_cache_extension unless (name.split('/').last || name).include? '.'
return name
end
@ -117,21 +120,36 @@ module ActionController #:nodoc:
# expire_page :controller => "lists", :action => "show"
def expire_page(options = {})
return unless perform_caching
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
if options.is_a?(Hash)
if options[:action].is_a?(Array)
options[:action].dup.each do |action|
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
end
else
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
end
else
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
self.class.expire_page(options)
end
end
# Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used
# If no options are provided, the current +options+ for this action is used. Example:
# If no options are provided, the requested url is used. Example:
# cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = {})
def cache_page(content = nil, options = nil)
return unless perform_caching && caching_allowed
self.class.cache_page(content || response.body, url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])))
path = case options
when Hash
url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format]))
when String
options
else
request.path
end
self.class.cache_page(content || response.body, path)
end
private
@ -161,17 +179,26 @@ module ActionController #:nodoc:
# Different representations of the same resource, e.g. <tt>http://david.somewhere.com/lists</tt> and <tt>http://david.somewhere.com/lists.xml</tt>
# are treated like separate requests and so are cached separately. Keep in mind when expiring an action cache that <tt>:action => 'lists'</tt> is not the same
# as <tt>:action => 'list', :format => :xml</tt>.
#
# You can set modify the default action cache path by passing a :cache_path option. This will be passed directly to ActionCachePath.path_for. This is handy
# for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance.
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
# caches_page :public
# caches_action :show, :cache_path => { :project => 1 }
# caches_action :show, :cache_path => Proc.new { |controller|
# controller.params[:user_id] ?
# controller.send(:user_list_url, c.params[:user_id], c.params[:id]) :
# controller.send(:list_url, c.params[:id]) }
# end
module Actions
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
attr_accessor :rendered_action_cache, :action_cache_path
alias_method_chain :protected_instance_variables, :action_caching
end
end
def protected_instance_variables_with_action_caching
protected_instance_variables_without_action_caching + %w(@action_cache_path)
base.class_eval do
attr_accessor :rendered_action_cache, :action_cache_path
alias_method_chain :protected_instance_variables, :action_caching
end
end
module ClassMethods
@ -179,12 +206,14 @@ module ActionController #:nodoc:
# See ActionController::Caching::Actions for details.
def caches_action(*actions)
return unless perform_caching
action_cache_filter = ActionCacheFilter.new(*actions)
before_filter action_cache_filter
after_filter action_cache_filter
around_filter(ActionCacheFilter.new(*actions))
end
end
def protected_instance_variables_with_action_caching
protected_instance_variables_without_action_caching + %w(@action_cache_path)
end
def expire_action(options = {})
return unless perform_caching
if options[:action].is_a?(Array)
@ -197,17 +226,18 @@ module ActionController #:nodoc:
end
class ActionCacheFilter #:nodoc:
def initialize(*actions)
def initialize(*actions, &block)
@options = actions.extract_options!
@actions = Set.new actions
end
def before(controller)
return unless @actions.include?(controller.action_name.to_sym)
cache_path = ActionCachePath.new(controller, {})
return unless @actions.include?(controller.action_name.intern)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options))
if cache = controller.read_fragment(cache_path.path)
controller.rendered_action_cache = true
set_content_type!(controller, cache_path.extension)
controller.send(:render_text, cache)
controller.send!(:render_for_text, cache)
false
else
controller.action_cache_path = cache_path
@ -215,15 +245,22 @@ module ActionController #:nodoc:
end
def after(controller)
return if !@actions.include?(controller.action_name.to_sym) || controller.rendered_action_cache
return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache || !caching_allowed(controller)
controller.write_fragment(controller.action_cache_path.path, controller.response.body)
end
private
def set_content_type!(controller, extension)
controller.response.content_type = Mime::EXTENSION_LOOKUP[extension].to_s if extension
controller.response.content_type = Mime::Type.lookup_by_extension(extension).to_s if extension
end
def path_options_for(controller, options)
((path_options = options[:cache_path]).respond_to?(:call) ? path_options.call(controller) : path_options) || {}
end
def caching_allowed(controller)
controller.request.get? && controller.response.headers['Status'].to_i == 200
end
end
class ActionCachePath
@ -253,7 +290,7 @@ module ActionController #:nodoc:
end
def extract_extension(file_path)
# Don't want just what comes after the last '.' to accomodate multi part extensions
# Don't want just what comes after the last '.' to accommodate multi part extensions
# such as tar.gz.
file_path[/^[^.]+\.(.+)$/, 1]
end
@ -270,25 +307,28 @@ module ActionController #:nodoc:
# <%= render :partial => "topic", :collection => Topic.find(:all) %>
# <% end %>
#
# This cache will bind to the name of action that called it. So you would be able to invalidate it using
# <tt>expire_fragment(:controller => "topics", :action => "list")</tt> -- if that was the controller/action used. This is not too helpful
# if you need to cache multiple fragments per action or if the action itself is cached using <tt>caches_action</tt>. So instead we should
# qualify the name of the action used with something like:
# This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would
# be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>.
#
# This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using
# <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like:
#
# <% cache(:action => "list", :action_suffix => "all_topics") do %>
#
# That would result in a name such as "/topics/list/all_topics", which wouldn't conflict with any action cache and neither with another
# fragment using a different suffix. Note that the URL doesn't have to really exist or be callable. We're just using the url_for system
# to generate unique cache names that we can refer to later for expirations. The expiration call for this example would be
# <tt>expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")</tt>.
# That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a
# different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique
# cache names that we can refer to when we need to expire the cache.
#
# The expiration call for this example is:
#
# expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")
#
# == Fragment stores
#
# In order to use the fragment caching, you need to designate where the caches should be stored. This is done by assigning a fragment store
# of which there are four different kinds:
# By default, cached fragments are stored in memory. The available store options are:
#
# * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and shares the fragments for
# all the web server processes running off the same application directory.
# * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and allows all
# processes running from the same application directory to access the cached content.
# * MemoryStore: Keeps the fragments in memory, which is fine for WEBrick and for FCGI (if you don't care that each FCGI process holds its
# own fragment store). It's not suitable for CGI as the process is thrown away at the end of each request. It can potentially also take
# up a lot of memory since each process keeps all the caches in memory.
@ -310,6 +350,7 @@ module ActionController #:nodoc:
@@fragment_cache_store = MemoryStore.new
cattr_reader :fragment_cache_store
# Defines the storage option for cached fragments
def self.fragment_cache_store=(store_option)
store, *parameters = *([ store_option ].flatten)
@@fragment_cache_store = if store.is_a?(Symbol)
@ -323,6 +364,9 @@ module ActionController #:nodoc:
end
end
# Given a name (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
# writing, or expiring a cached fragment. If the name is a hash, the generated name is the return
# value of url_for on that hash (without the protocol).
def fragment_cache_key(name)
name.is_a?(Hash) ? url_for(name).split("://").last : name
end
@ -331,7 +375,7 @@ module ActionController #:nodoc:
def cache_erb_fragment(block, name = {}, options = nil)
unless perform_caching then block.call; return end
buffer = eval("_erbout", block.binding)
buffer = eval(ActionView::Base.erb_variable, block.binding)
if cache = read_fragment(name, options)
buffer.concat(cache)
@ -342,6 +386,7 @@ module ActionController #:nodoc:
end
end
# Writes <tt>content</tt> to the location signified by <tt>name</tt> (see <tt>expire_fragment</tt> for acceptable formats)
def write_fragment(name, content, options = nil)
return unless perform_caching
@ -352,6 +397,7 @@ module ActionController #:nodoc:
content
end
# Reads a cached fragment from the location signified by <tt>name</tt> (see <tt>expire_fragment</tt> for acceptable formats)
def read_fragment(name, options = nil)
return unless perform_caching
@ -368,8 +414,8 @@ module ActionController #:nodoc:
# %r{pages/\d*/notes}
# Ensure you do not specify start and finish in the regex (^$) because
# the actual filename matched looks like ./cache/filename/path.cache
# Regexp expiration is not supported on caches which can't iterate over
# all keys, such as memcached.
# Regexp expiration is only supported on caches that can iterate over
# all keys (unlike memcached).
def expire_fragment(name, options = nil)
return unless perform_caching
@ -386,12 +432,6 @@ module ActionController #:nodoc:
end
end
# Deprecated -- just call expire_fragment with a regular expression
def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
expire_fragment(matcher, options)
end
deprecate :expire_matched_fragments => :expire_fragment
class UnthreadedMemoryStore #:nodoc:
def initialize #:nodoc:
@ -438,7 +478,7 @@ module ActionController #:nodoc:
super
if ActionController::Base.allow_concurrency
@mutex = Mutex.new
MemoryStore.send(:include, ThreadSafety)
MemoryStore.module_eval { include ThreadSafety }
end
end
end
@ -453,6 +493,8 @@ module ActionController #:nodoc:
end
end
begin
require_library_or_gem 'memcache'
class MemCacheStore < MemoryStore #:nodoc:
attr_reader :addresses
@ -464,6 +506,9 @@ module ActionController #:nodoc:
@data = MemCache.new(*addresses)
end
end
rescue LoadError
# MemCache wasn't available so neither can the store be
end
class UnthreadedFileStore #:nodoc:
attr_reader :cache_path
@ -507,7 +552,7 @@ module ActionController #:nodoc:
end
def ensure_cache_path(path)
FileUtils.makedirs(path) unless File.exists?(path)
FileUtils.makedirs(path) unless File.exist?(path)
end
def search_dir(dir, &callback)
@ -528,7 +573,7 @@ module ActionController #:nodoc:
super(cache_path)
if ActionController::Base.allow_concurrency
@mutex = Mutex.new
FileStore.send(:include, ThreadSafety)
FileStore.module_eval { include ThreadSafety }
end
end
end
@ -564,7 +609,7 @@ module ActionController #:nodoc:
module ClassMethods #:nodoc:
def cache_sweeper(*sweepers)
return unless perform_caching
configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
configuration = sweepers.extract_options!
sweepers.each do |sweeper|
ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
@ -583,10 +628,6 @@ module ActionController #:nodoc:
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
# ActiveRecord::Observer will mark this class as reloadable even though it should not be.
# However, subclasses of ActionController::Caching::Sweeper should be Reloadable
include Reloadable::Deprecated
def before(controller)
self.controller = controller
callback(:before)
@ -598,20 +639,45 @@ module ActionController #:nodoc:
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
send(controller_callback_method_name) if respond_to?(controller_callback_method_name)
send(action_callback_method_name) if respond_to?(action_callback_method_name)
send!(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
send!(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments)
return if @controller.nil?
@controller.send(method, *arguments)
@controller.send!(method, *arguments)
end
end
end
module SqlCache
def self.included(base) #:nodoc:
if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache)
base.alias_method_chain :perform_action, :caching
end
end
def perform_action_with_caching
ActiveRecord::Base.cache do
perform_action_without_caching
end
end
end
end
end

View file

@ -0,0 +1,16 @@
require 'action_controller/cgi_ext/stdinput'
require 'action_controller/cgi_ext/query_extension'
require 'action_controller/cgi_ext/cookie'
require 'action_controller/cgi_ext/session'
class CGI #:nodoc:
include ActionController::CgiExt::Stdinput
class << self
alias :escapeHTML_fail_on_nil :escapeHTML
def escapeHTML(string)
escapeHTML_fail_on_nil(string) unless string.nil?
end
end
end

View file

@ -1,36 +0,0 @@
require 'cgi'
require 'cgi/session'
require 'cgi/session/pstore'
require 'action_controller/cgi_ext/cgi_methods'
# Wrapper around the CGIMethods that have been secluded to allow testing without
# an instantiated CGI object
class CGI #:nodoc:
class << self
alias :escapeHTML_fail_on_nil :escapeHTML
def escapeHTML(string)
escapeHTML_fail_on_nil(string) unless string.nil?
end
end
# Returns a parameter hash including values from both the request (POST/GET)
# and the query string with the latter taking precedence.
def parameters
request_parameters.update(query_parameters)
end
def query_parameters
CGIMethods.parse_query_parameters(query_string)
end
def request_parameters
CGIMethods.parse_request_parameters(params, env_table)
end
def session(parameters = nil)
parameters = {} if parameters.nil?
parameters['database_manager'] = CGI::Session::PStore
CGI::Session.new(self, parameters)
end
end

View file

@ -1,211 +0,0 @@
require 'cgi'
require 'action_controller/vendor/xml_node'
require 'strscan'
# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
class << self
# DEPRECATED: Use parse_form_encoded_parameters
def parse_query_parameters(query_string)
pairs = query_string.split('&').collect do |chunk|
next if chunk.empty?
key, value = chunk.split('=', 2)
next if key.empty?
value = (value.nil? || value.empty?) ? nil : CGI.unescape(value)
[ CGI.unescape(key), value ]
end.compact
FormEncodedPairParser.new(pairs).result
end
# DEPRECATED: Use parse_form_encoded_parameters
def parse_request_parameters(params)
parser = FormEncodedPairParser.new
params = params.dup
until params.empty?
for key, value in params
if key.blank?
params.delete key
elsif !key.include?('[')
# much faster to test for the most common case first (GET)
# and avoid the call to build_deep_hash
parser.result[key] = get_typed_value(value[0])
params.delete key
elsif value.is_a?(Array)
parser.parse(key, get_typed_value(value.shift))
params.delete key if value.empty?
else
raise TypeError, "Expected array, found #{value.inspect}"
end
end
end
parser.result
end
def parse_formatted_request_parameters(mime_type, raw_post_data)
case strategy = ActionController::Base.param_parsers[mime_type]
when Proc
strategy.call(raw_post_data)
when :xml_simple
raw_post_data.blank? ? {} : Hash.from_xml(raw_post_data)
when :yaml
YAML.load(raw_post_data)
when :xml_node
node = XmlNode.from_xml(raw_post_data)
{ node.node_name => node }
end
rescue Exception => e # YAML, XML or Ruby code block errors
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
"raw_post_data" => raw_post_data, "format" => mime_type }
end
private
def get_typed_value(value)
case value
when String
value
when NilClass
''
when Array
value.map { |v| get_typed_value(v) }
else
# Uploaded file provides content type and filename.
if value.respond_to?(:content_type) &&
!value.content_type.blank? &&
!value.original_filename.blank?
unless value.respond_to?(:full_original_filename)
class << value
alias_method :full_original_filename, :original_filename
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
md.captures.first
else
File.basename full_original_filename
end
end
end
end
# Return the same value after overriding original_filename.
value
# Multipart values may have content type, but no filename.
elsif value.respond_to?(:read)
result = value.read
value.rewind
result
# Unknown value, neither string nor multipart.
else
raise "Unknown form value: #{value.inspect}"
end
end
end
end
class FormEncodedPairParser < StringScanner #:nodoc:
attr_reader :top, :parent, :result
def initialize(pairs = [])
super('')
@result = {}
pairs.each { |key, value| parse(key, value) }
end
KEY_REGEXP = %r{([^\[\]=&]+)}
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
# Parse the query string
def parse(key, value)
self.string = key
@top, @parent = result, nil
# First scan the bare key
key = scan(KEY_REGEXP) or return
key = post_key_check(key)
# Then scan as many nestings as present
until eos?
r = scan(BRACKETED_KEY_REGEXP) or return
key = self[1]
key = post_key_check(key)
end
bind(key, value)
end
private
# After we see a key, we must look ahead to determine our next action. Cases:
#
# [] follows the key. Then the value must be an array.
# = follows the key. (A value comes next)
# & or the end of string follows the key. Then the key is a flag.
# otherwise, a hash follows the key.
def post_key_check(key)
if scan(/\[\]/) # a[b][] indicates that b is an array
container(key, Array)
nil
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
container(key, Hash)
nil
else # End of key? We do nothing.
key
end
end
# Add a container to the stack.
#
def container(key, klass)
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
value = bind(key, klass.new)
type_conflict! klass, value unless value.is_a?(klass)
push(value)
end
# Push a value onto the 'stack', which is actually only the top 2 items.
def push(value)
@parent, @top = @top, value
end
# Bind a key (which may be nil for items in an array) to the provided value.
def bind(key, value)
if top.is_a? Array
if key
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
top[-1][key] = value
else
top << {key => value}.with_indifferent_access
push top.last
end
else
top << value
end
elsif top.is_a? Hash
key = CGI.unescape(key)
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
return top[key] ||= value
else
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
end
return value
end
def type_conflict!(klass, value)
raise TypeError,
"Conflicting types for parameter containers. " +
"Expected an instance of #{klass}, but found an instance of #{value.class}. " +
"This can be caused by passing Array and Hash based paramters qs[]=value&qs[key]=value. "
end
end
end

View file

@ -1,10 +1,11 @@
CGI.module_eval { remove_const "Cookie" }
# TODO: document how this differs from stdlib CGI::Cookie
class CGI #:nodoc:
# This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below.
# It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on
# http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
class Cookie < DelegateClass(Array)
attr_accessor :name, :value, :path, :domain, :expires
attr_reader :secure, :http_only
# Create a new CGI::Cookie object.
#
# The contents of the cookie can be specified as a +name+ and one
@ -19,9 +20,11 @@ class CGI #:nodoc:
# domain:: the domain for which this cookie applies.
# expires:: the time at which this cookie expires, as a +Time+ object.
# secure:: whether this cookie is a secure cookie or not (default to
# false). Secure cookies are only transmitted to HTTPS
# false). Secure cookies are only transmitted to HTTPS
# servers.
#
# http_only:: whether this cookie can be accessed by client side scripts (e.g. document.cookie) or only over HTTP
# More details: http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx
# Defaults to false.
# These keywords correspond to attributes of the cookie object.
def initialize(name = '', *value)
if name.kind_of?(String)
@ -30,6 +33,7 @@ class CGI #:nodoc:
@domain = nil
@expires = nil
@secure = false
@http_only = false
@path = nil
else
@name = name['name']
@ -37,12 +41,11 @@ class CGI #:nodoc:
@domain = name['domain']
@expires = name['expires']
@secure = name['secure'] || false
@http_only = name['http_only'] || false
@path = name['path']
end
unless @name
raise ArgumentError, "`name' required"
end
raise ArgumentError, "`name' required" unless @name
# simple support for IE
unless @path
@ -53,48 +56,26 @@ class CGI #:nodoc:
super(@value)
end
def __setobj__(obj)
@_dc_obj = obj
# Set whether the Cookie is a secure cookie or not.
def secure=(val)
@secure = val == true
end
attr_accessor("name", "value", "path", "domain", "expires")
attr_reader("secure")
# Set whether the Cookie is a secure cookie or not.
#
# +val+ must be a boolean.
def secure=(val)
@secure = val if val == true or val == false
@secure
# Set whether the Cookie is an HTTP only cookie or not.
def http_only=(val)
@http_only = val == true
end
# Convert the Cookie to its string representation.
def to_s
buf = ""
buf = ''
buf << @name << '='
if @value.kind_of?(String)
buf << CGI::escape(@value)
else
buf << @value.collect{|v| CGI::escape(v) }.join("&")
end
if @domain
buf << '; domain=' << @domain
end
if @path
buf << '; path=' << @path
end
if @expires
buf << '; expires=' << CGI::rfc1123_date(@expires)
end
if @secure == true
buf << '; secure'
end
buf << (@value.kind_of?(String) ? CGI::escape(@value) : @value.collect{|v| CGI::escape(v) }.join("&"))
buf << '; domain=' << @domain if @domain
buf << '; path=' << @path if @path
buf << '; expires=' << CGI::rfc1123_date(@expires) if @expires
buf << '; secure' if @secure
buf << '; HttpOnly' if @http_only
buf
end
@ -108,7 +89,7 @@ class CGI #:nodoc:
cookies = Hash.new([])
if raw_cookie
raw_cookie.split(/; ?/).each do |pairs|
raw_cookie.split(/[;,]\s?/).each do |pairs|
name, values = pairs.split('=',2)
next unless name and values
name = CGI::unescape(name)

View file

@ -1,30 +0,0 @@
# CGI::Session::PStore.initialize requires 'digest/md5' on every call.
# This makes sense when spawning processes per request, but is
# unnecessarily expensive when serving requests from a long-lived
# process.
require 'cgi/session'
require 'cgi/session/pstore'
require 'digest/md5'
class CGI::Session::PStore #:nodoc:
def initialize(session, option={})
dir = option['tmpdir'] || Dir::tmpdir
prefix = option['prefix'] || ''
id = session.session_id
md5 = Digest::MD5.hexdigest(id)[0,16]
path = dir+"/"+prefix+md5
path.untaint
if File::exist?(path)
@hash = nil
else
unless session.new_session
raise CGI::Session::NoSession, "uninitialized session"
end
@hash = {}
end
@p = ::PStore.new(path)
@p.transaction do |p|
File.chmod(0600, p.path)
end
end
end

View file

@ -0,0 +1,22 @@
require 'cgi'
class CGI #:nodoc:
module QueryExtension
# Remove the old initialize_query method before redefining it.
remove_method :initialize_query
# Neuter CGI parameter parsing.
def initialize_query
# Fix some strange request environments.
env_table['REQUEST_METHOD'] ||= 'GET'
# POST assumes missing Content-Type is application/x-www-form-urlencoded.
if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST'
env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
end
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
@params = {}
end
end
end

View file

@ -1,95 +0,0 @@
class CGI #:nodoc:
module QueryExtension
# Initialize the data from the query.
#
# Handles multipart forms (in particular, forms that involve file uploads).
# Reads query parameters in the @params field, and cookies into @cookies.
def initialize_query
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
# Fix some strange request environments.
if method = env_table['REQUEST_METHOD']
method = method.to_s.downcase.intern
else
method = :get
end
# POST assumes missing Content-Type is application/x-www-form-urlencoded.
content_type = env_table['CONTENT_TYPE']
if content_type.blank? && method == :post
content_type = 'application/x-www-form-urlencoded'
end
# Force content length to zero if missing.
content_length = env_table['CONTENT_LENGTH'].to_i
# Set multipart to false by default.
@multipart = false
# POST and PUT may have params in entity body. If content type is
# missing for POST, assume urlencoded. If content type is missing
# for PUT, don't assume anything and don't parse the parameters:
# it's likely binary data.
#
# The other HTTP methods have their params in the query string.
if method == :post || method == :put
if boundary = extract_multipart_form_boundary(content_type)
@multipart = true
@params = read_multipart(boundary, content_length)
elsif content_type.blank? || content_type !~ %r{application/x-www-form-urlencoded}i
read_params(method, content_length)
@params = {}
end
end
@params ||= CGI.parse(read_params(method, content_length))
end
private
unless defined?(MULTIPART_FORM_BOUNDARY_RE)
MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #"
end
def extract_multipart_form_boundary(content_type)
MULTIPART_FORM_BOUNDARY_RE.match(content_type).to_a.pop
end
if defined? MOD_RUBY
def read_query
Apache::request.args || ''
end
else
def read_query
# fixes CGI querystring parsing for lighttpd
env_qs = env_table['QUERY_STRING']
if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
uri.split('?', 2)[1] || ''
else
env_qs || ''
end
end
end
def read_body(content_length)
stdinput.binmode if stdinput.respond_to?(:binmode)
content = stdinput.read(content_length) || ''
# Fix for Safari Ajax postings that always append \000
content.chop! if content[-1] == 0
content.gsub!(/&_=$/, '')
env_table['RAW_POST_DATA'] = content.freeze
end
def read_params(method, content_length)
case method
when :get
read_query
when :post, :put
read_body(content_length)
when :cmd
read_from_cmdline
else # :head, :delete, :options, :trace, :connect
read_query
end
end
end # module QueryExtension
end

View file

@ -0,0 +1,73 @@
require 'digest/md5'
require 'cgi/session'
require 'cgi/session/pstore'
class CGI #:nodoc:
# * Expose the CGI instance to session stores.
# * Don't require 'digest/md5' whenever a new session id is generated.
class Session #:nodoc:
begin
require 'securerandom'
# Generate a 32-character unique id using SecureRandom.
# This is used to generate session ids but may be reused elsewhere.
def self.generate_unique_id(constant = nil)
SecureRandom.hex(16)
end
rescue LoadError
# Generate an 32-character unique id based on a hash of the current time,
# a random number, the process id, and a constant string. This is used
# to generate session ids but may be reused elsewhere.
def self.generate_unique_id(constant = 'foobar')
md5 = Digest::MD5.new
now = Time.now
md5 << now.to_s
md5 << String(now.usec)
md5 << String(rand(0))
md5 << String($$)
md5 << constant
md5.hexdigest
end
end
# Make the CGI instance available to session stores.
attr_reader :cgi
attr_reader :dbman
alias_method :initialize_without_cgi_reader, :initialize
def initialize(cgi, options = {})
@cgi = cgi
initialize_without_cgi_reader(cgi, options)
end
private
# Create a new session id.
def create_new_id
@new_session = true
self.class.generate_unique_id
end
# * Don't require 'digest/md5' whenever a new session is started.
class PStore #:nodoc:
def initialize(session, option={})
dir = option['tmpdir'] || Dir::tmpdir
prefix = option['prefix'] || ''
id = session.session_id
md5 = Digest::MD5.hexdigest(id)[0,16]
path = dir+"/"+prefix+md5
path.untaint
if File::exist?(path)
@hash = nil
else
unless session.new_session
raise CGI::Session::NoSession, "uninitialized session"
end
@hash = {}
end
@p = ::PStore.new(path)
@p.transaction do |p|
File.chmod(0600, p.path)
end
end
end
end
end

View file

@ -1,30 +0,0 @@
# CGI::Session#create_new_id requires 'digest/md5' on every call. This makes
# sense when spawning processes per request, but is unnecessarily expensive
# when serving requests from a long-lived process.
#
# http://railsexpress.de/blog/articles/2005/11/22/speeding-up-the-creation-of-new-sessions
require 'cgi/session'
require 'digest/md5'
class CGI
class Session #:nodoc:
private
# Create a new session id.
#
# The session id is an MD5 hash based upon the time,
# a random number, and a constant string. This routine
# is used internally for automatically generated
# session ids.
def create_new_id
md5 = Digest::MD5::new
now = Time::now
md5.update(now.to_s)
md5.update(String(now.usec))
md5.update(String(rand(0)))
md5.update(String($$))
md5.update('foobar')
@new_session = true
md5.hexdigest
end
end
end

View file

@ -0,0 +1,23 @@
require 'cgi'
module ActionController
module CgiExt
# Publicize the CGI's internal input stream so we can lazy-read
# request.body. Make it writable so we don't have to play $stdin games.
module Stdinput
def self.included(base)
base.class_eval do
remove_method :stdinput
attr_accessor :stdinput
end
base.alias_method_chain :initialize, :stdinput
end
def initialize_with_stdinput(type = nil, stdinput = $stdin)
@stdinput = stdinput
initialize_without_stdinput(type || 'query')
end
end
end
end

View file

@ -1,8 +1,5 @@
require 'action_controller/cgi_ext/cgi_ext'
require 'action_controller/cgi_ext/cookie_performance_fix'
require 'action_controller/cgi_ext/raw_post_data_fix'
require 'action_controller/cgi_ext/session_performance_fix'
require 'action_controller/cgi_ext/pstore_performance_fix'
require 'action_controller/cgi_ext'
require 'action_controller/session/cookie_store'
module ActionController #:nodoc:
class Base
@ -36,55 +33,56 @@ module ActionController #:nodoc:
end
class CgiRequest < AbstractRequest #:nodoc:
attr_accessor :cgi, :session_options, :cookie_only
attr_accessor :cgi, :session_options
class SessionFixationAttempt < StandardError; end #:nodoc:
DEFAULT_SESSION_OPTIONS = {
:database_manager => CGI::Session::PStore,
:prefix => "ruby_sess.",
:session_path => "/",
:database_manager => CGI::Session::CookieStore, # store data in cookie
:prefix => "ruby_sess.", # prefix session file names
:session_path => "/", # available to all paths in app
:session_key => "_session_id",
:cookie_only => true
} unless const_defined?(:DEFAULT_SESSION_OPTIONS)
def initialize(cgi, session_options = {})
@cgi = cgi
@session_options = session_options
@env = @cgi.send(:env_table)
@cookie_only = session_options.delete :cookie_only
@env = @cgi.send!(:env_table)
super()
end
def query_string
if (qs = @cgi.query_string) && !qs.empty?
qs = @cgi.query_string if @cgi.respond_to?(:query_string)
if !qs.blank?
qs
elsif uri = @env['REQUEST_URI']
parts = uri.split('?')
parts.shift
parts.join('?')
else
@env['QUERY_STRING'] || ''
super
end
end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
def body
if raw_post = env['RAW_POST_DATA']
StringIO.new(raw_post)
else
@cgi.stdinput
end
end
def query_parameters
@query_parameters ||=
(qs = self.query_string).empty? ? {} : CGIMethods.parse_query_parameters(qs)
@query_parameters ||= self.class.parse_query_parameters(query_string)
end
def request_parameters
@request_parameters ||=
if ActionController::Base.param_parsers.has_key?(content_type)
CGIMethods.parse_formatted_request_parameters(content_type, @env['RAW_POST_DATA'])
else
CGIMethods.parse_request_parameters(@cgi.params)
end
@request_parameters ||= parse_formatted_request_parameters
end
def cookies
@cgi.cookies.freeze
end
def host_with_port
def host_with_port_without_standard_port_handling
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
elsif http_host = env['HTTP_HOST']
@ -97,11 +95,11 @@ module ActionController #:nodoc:
end
def host
host_with_port[/^[^:]+/]
host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
end
def port
if host_with_port =~ /:(\d+)$/
if host_with_port_without_standard_port_handling =~ /:(\d+)$/
$1.to_i
else
standard_port
@ -114,7 +112,7 @@ module ActionController #:nodoc:
@session = Hash.new
else
stale_session_check! do
if @cookie_only && request_parameters[session_options_with_string_keys['session_key']]
if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
raise SessionFixationAttempt
end
case value = session_options_with_string_keys['new_session']
@ -146,7 +144,7 @@ module ActionController #:nodoc:
end
def method_missing(method_id, *arguments)
@cgi.send(method_id, *arguments) rescue super
@cgi.send!(method_id, *arguments) rescue super
end
private
@ -160,12 +158,17 @@ module ActionController #:nodoc:
end
end
def cookie_only?
session_options_with_string_keys['cookie_only']
end
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]+)}
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
Module.const_missing($1)
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, <<-end_msg
Session contains objects whose class definition isn\'t available.
@ -192,16 +195,13 @@ end_msg
end
def out(output = $stdout)
convert_content_type!
set_content_length!
output.binmode if output.respond_to?(:binmode)
output.sync = false if output.respond_to?(:sync=)
begin
output.write(@cgi.header(@headers))
if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD'
if @cgi.send!(:env_table)['REQUEST_METHOD'] == 'HEAD'
return
elsif @body.respond_to?(:call)
# Flush the output now in case the @body Proc uses
@ -217,24 +217,5 @@ end_msg
# lost connection to parent process, ignore output
end
end
private
def convert_content_type!
if content_type = @headers.delete("Content-Type")
@headers["type"] = content_type
end
if content_type = @headers.delete("Content-type")
@headers["type"] = content_type
end
if content_type = @headers.delete("content-type")
@headers["type"] = content_type
end
end
# Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
# for, say, a 2GB streaming file.
def set_content_length!
@headers["Content-Length"] = @body.size unless @body.respond_to?(:call)
end
end
end

View file

@ -1,235 +0,0 @@
module ActionController
module CodeGeneration #:nodoc:
class GenerationError < StandardError #:nodoc:
end
class Source #:nodoc:
attr_reader :lines, :indentation_level
IndentationString = ' '
def initialize
@lines, @indentation_level = [], 0
end
def line(line)
@lines << (IndentationString * @indentation_level + line)
end
alias :<< :line
def indent
@indentation_level += 1
yield
ensure
@indentation_level -= 1
end
def to_s() lines.join("\n") end
end
class CodeGenerator #:nodoc:
attr_accessor :source, :locals
def initialize(source = nil)
@locals = []
@source = source || Source.new
end
BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
Keywords = BeginKeywords + ResumeKeywords
def method_missing(keyword, *text)
if Keywords.include? keyword
if ResumeKeywords.include? keyword
raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/
source.lines.pop # Remove the 'end'
end
line "#{keyword} #{text.join ' '}"
begin source.indent { yield(self.dup) }
ensure line 'end'
end
else
super(keyword, *text)
end
end
def line(*args) self.source.line(*args) end
alias :<< :line
def indent(*args, &block) source(*args, &block) end
def to_s() source.to_s end
def share_locals_with(other)
other.locals = self.locals = (other.locals | locals)
end
FieldsToDuplicate = [:locals]
def dup
copy = self.class.new(source)
self.class::FieldsToDuplicate.each do |sym|
value = self.send(sym)
value = value.dup unless value.nil? || value.is_a?(Numeric)
copy.send("#{sym}=", value)
end
return copy
end
end
class RecognitionGenerator < CodeGenerator #:nodoc:
Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
attr_accessor(*Attributes)
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
def initialize(*args)
super(*args)
@after, @before = [], []
@current = nil
@results, @constants = {}, {}
@depth = 0
@move_ahead = nil
@finish_statement = Proc.new {|hash_expr| hash_expr}
end
def if_next_matches(string, &block)
test = Routing.test_condition(next_segment(true), string)
self.if(test, &block)
end
def move_forward(places = 1)
dup = self.dup
dup.depth += 1
dup.move_ahead = places
yield dup
end
def next_segment(assign_inline = false, default = nil)
if locals.include?(segment_name)
code = segment_name
else
code = "#{segment_name} = #{path_name}[#{index_name}]"
if assign_inline
code = "(#{code})"
else
line(code)
code = segment_name
end
locals << segment_name
end
code = "(#{code} || #{default.inspect})" if default
return code.to_s
end
def segment_name() "segment#{depth}".to_sym end
def path_name() :path end
def index_name
move_ahead, @move_ahead = @move_ahead, nil
move_ahead ? "index += #{move_ahead}" : 'index'
end
def continue
dup = self.dup
dup.before << dup.current
dup.current = dup.after.shift
dup.go
end
def go
if current then current.write_recognition(self)
else self.finish
end
end
def result(key, expression, delay = false)
unless delay
line "#{key}_value = #{expression}"
expression = "#{key}_value"
end
results[key] = expression
end
def constant_result(key, object)
constants[key] = object
end
def finish(ensure_traversal_finished = true)
pairs = []
(results.keys + constants.keys).uniq.each do |key|
pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
end
hash_expr = "{#{pairs.join(', ')}}"
statement = finish_statement.call(hash_expr)
if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
else self << statement
end
end
end
class GenerationGenerator < CodeGenerator #:nodoc:
Attributes = [:after, :before, :current, :segments]
attr_accessor(*Attributes)
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
def initialize(*args)
super(*args)
@after, @before = [], []
@current = nil
@segments = []
end
def hash_name() 'hash' end
def local_name(key) "#{key}_value" end
def hash_value(key, assign = true, default = nil)
if locals.include?(local_name(key)) then code = local_name(key)
else
code = "hash[#{key.to_sym.inspect}]"
if assign
code = "(#{local_name(key)} = #{code})"
locals << local_name(key)
end
end
code = "(#{code} || (#{default.inspect}))" if default
return code
end
def expire_for_keys(*keys)
return if keys.empty?
conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
end
def add_segment(*segments)
d = dup
d.segments.concat segments
yield d
end
def go
if current then current.write_generation(self)
else self.finish
end
end
def continue
d = dup
d.before << d.current
d.current = d.after.shift
d.go
end
def finish
line %("/#{segments.join('/')}")
end
def check_conditions(conditions)
tests = []
generator = nil
conditions.each do |key, condition|
tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
generator = self.dup unless generator
end
return tests.join(' && ')
end
end
end
end

View file

@ -17,44 +17,44 @@ module ActionController #:nodoc:
# end
#
# The same can be done in a view to do a partial rendering:
#
# Let's see a greeting:
#
# Let's see a greeting:
# <%= render_component :controller => "greeter", :action => "hello_world" %>
#
# It is also possible to specify the controller as a class constant, bypassing the inflector
# code to compute the controller class at runtime:
#
#
# <%= render_component :controller => GreeterController, :action => "hello_world" %>
#
# == When to use components
#
# Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
# conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
# reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
# reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
# across many applications at once.
#
# So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
module Components
def self.included(base) #:nodoc:
base.send :include, InstanceMethods
base.extend(ClassMethods)
base.helper do
def render_component(options)
@controller.send(:render_component_as_string, options)
end
end
# If this controller was instantiated to process a component request,
# +parent_controller+ points to the instantiator of this controller.
base.send :attr_accessor, :parent_controller
base.class_eval do
include InstanceMethods
extend ClassMethods
helper do
def render_component(options)
@controller.send!(:render_component_as_string, options)
end
end
# If this controller was instantiated to process a component request,
# +parent_controller+ points to the instantiator of this controller.
attr_accessor :parent_controller
alias_method_chain :process_cleanup, :components
alias_method_chain :set_session_options, :components
alias_method_chain :flash, :components
alias_method :component_request?, :parent_controller
alias_method :component_request?, :parent_controller
end
end
@ -65,23 +65,6 @@ module ActionController #:nodoc:
controller.parent_controller = parent_controller
controller.process(request, response)
end
# Set the template root to be one directory behind the root dir of the controller. Examples:
# /code/weblog/components/admin/users_controller.rb with Admin::UsersController
# will use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/users/
#
# /code/weblog/components/admin/parties/users_controller.rb with Admin::Parties::UsersController
# will also use /code/weblog/components as template root
# and find templates in /code/weblog/components/admin/parties/users/
def uses_component_template_root
path_of_calling_controller = File.dirname(caller[1].split(/:\d+:/, 2).first)
path_of_controller_root = path_of_calling_controller.sub(/#{Regexp.escape(File.dirname(controller_path))}$/, "")
self.template_root = path_of_controller_root
end
deprecate :uses_component_template_root => 'Components are deprecated and will be removed in Rails 2.0.'
end
module InstanceMethods
@ -90,12 +73,12 @@ module ActionController #:nodoc:
flash.discard if component_request?
process_without_components(request, response, method, *arguments)
end
protected
# Renders the component specified as the response for the current method
def render_component(options) #:doc:
component_logging(options) do
render_text(component_response(options, true).body, response.headers["Status"])
render_for_text(component_response(options, true).body, response.headers["Status"])
end
end

View file

@ -23,18 +23,19 @@ module ActionController #:nodoc:
# * <tt>domain</tt> - the domain for which this cookie applies.
# * <tt>expires</tt> - the time at which this cookie expires, as a +Time+ object.
# * <tt>secure</tt> - whether this cookie is a secure cookie or not (default to false).
# Secure cookies are only transmitted to HTTPS servers.
# Secure cookies are only transmitted to HTTPS servers.
# * <tt>http_only</tt> - whether this cookie is accessible via scripting or only HTTP (defaults to false).
module Cookies
def self.included(base)
base.helper_method :cookies
end
protected
# Returns the cookie container, which operates as described above.
def cookies
CookieJar.new(self)
end
# Deprecated cookie writer method
def cookie(*options)
response.headers['cookie'] << CGI::Cookie.new(*options)
end
end
class CookieJar < Hash #:nodoc:
@ -44,10 +45,13 @@ module ActionController #:nodoc:
update(@cookies)
end
# Returns the value of the cookie by +name+ -- or nil if no such cookie exists. You set new cookies using either the cookie method
# or cookies[]= (for simple name/value cookies without options).
# Returns the value of the cookie by +name+ -- or nil if no such cookie exists. You set new cookies using cookies[]=
# (for simple name/value cookies without options).
def [](name)
@cookies[name.to_s].value.first if @cookies[name.to_s] && @cookies[name.to_s].respond_to?(:value)
cookie = @cookies[name.to_s]
if cookie && cookie.respond_to?(:value)
cookie.size > 1 ? cookie.value : cookie.value[0]
end
end
def []=(name, options)

View file

@ -1,83 +0,0 @@
module ActionController #:nodoc:
module Dependencies #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Dependencies control what classes are needed for the controller to run its course. This is an alternative to doing explicit
# +require+ statements that bring a number of benefits. It's more succinct, communicates what type of dependency we're talking about,
# can trigger special behavior (as in the case of +observer+), and enables Rails to be clever about reloading in cached environments
# like FCGI. Example:
#
# class ApplicationController < ActionController::Base
# model :account, :company, :person, :project, :category
# helper :access_control
# service :notifications, :billings
# observer :project_change_observer
# end
#
# Please note that a controller like ApplicationController will automatically attempt to require_dependency on a model of its
# singuralized name and a helper of its name. If nothing is found, no error is raised. This is especially useful for concrete
# controllers like PostController:
#
# class PostController < ApplicationController
# # model :post (already required)
# # helper :post (already required)
# end
#
# Also note, that if the models follow the pattern of just 1 class per file in the form of MyClass => my_class.rb, then these
# classes don't have to be required as Active Support will auto-require them.
module ClassMethods #:nodoc:
# Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
# backend for modelling entity classes.
def model(*models)
require_dependencies(:model, models)
depend_on(:model, models)
end
# Specifies a variable number of services that this controller depends on. Services are normally singletons or factories, like
# Action Mailer service or a Payment Gateway service.
def service(*services)
require_dependencies(:service, services)
depend_on(:service, services)
end
# Specifies a variable number of observers that are to govern when this controller is handling actions. The observers will
# automatically have .instance called on them to make them active on assignment.
def observer(*observers)
require_dependencies(:observer, observers)
depend_on(:observer, observers)
instantiate_observers(observers)
end
# Returns an array of symbols that specify the dependencies on a given layer. For the example at the top, calling
# <tt>ApplicationController.dependencies_on(:model)</tt> would return <tt>[:account, :company, :person, :project, :category]</tt>
def dependencies_on(layer)
read_inheritable_attribute("#{layer}_dependencies")
end
def depend_on(layer, dependencies) #:nodoc:
write_inheritable_array("#{layer}_dependencies", dependencies)
end
private
def instantiate_observers(observers)
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
end
def require_dependencies(layer, dependencies)
dependencies.flatten.each do |dependency|
begin
require_dependency(dependency.to_s)
rescue LoadError => e
raise LoadError.new("Missing #{layer} #{dependency}.rb").copy_blame!(e)
rescue Object => exception
exception.blame_file! "=> #{layer} #{dependency}.rb"
raise
end
end
end
end
end
end

View file

@ -1,204 +0,0 @@
require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
module Test #:nodoc:
module Unit #:nodoc:
module Assertions
def assert_success(message=nil) #:nodoc:
assert_response(:success, message)
end
def assert_redirect(message=nil) #:nodoc:
assert_response(:redirect, message)
end
def assert_rendered_file(expected=nil, message=nil) #:nodoc:
assert_template(expected, message)
end
# ensure that the session has an object with the specified name
def assert_session_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the session <?>", key, @response.session)
assert_block(msg) { @response.has_session_object?(key) }
end
# ensure that the session has no object with the specified name
def assert_session_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the session <?>", key, @response.session)
assert_block(msg) { !@response.has_session_object?(key) }
end
def assert_session_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, @response.session[key])
assert_block(msg) { expected == @response.session[key] }
end
# -- cookie assertions ---------------------------------------------------
def assert_no_cookie(key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
msg = build_message(message, "<?> not expected in cookies['?']", actual, key)
assert_block(msg) { actual.nil? or actual.empty? }
end
def assert_cookie_equal(expected = nil, key = nil, message = nil) #:nodoc:
actual = @response.cookies[key]
actual = actual.first if actual
msg = build_message(message, "<?> expected in cookies['?'] but was <?>", expected, key, actual)
assert_block(msg) { expected == actual }
end
# -- flash assertions ---------------------------------------------------
# ensure that the flash has an object with the specified name
def assert_flash_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not in the flash <?>", key, @response.flash)
assert_block(msg) { @response.has_flash_object?(key) }
end
# ensure that the flash has no object with the specified name
def assert_flash_has_no(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is in the flash <?>", key, @response.flash)
assert_block(msg) { !@response.has_flash_object?(key) }
end
# ensure the flash exists
def assert_flash_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash does not exist <?>", @response.session['flash'] )
assert_block(msg) { @response.has_flash? }
end
# ensure the flash does not exist
def assert_flash_not_exists(message=nil) #:nodoc:
msg = build_message(message, "the flash exists <?>", @response.flash)
assert_block(msg) { !@response.has_flash? }
end
# ensure the flash is empty but existent
def assert_flash_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is not empty <?>", @response.flash)
assert_block(msg) { !@response.has_flash_with_contents? }
end
# ensure the flash is not empty
def assert_flash_not_empty(message=nil) #:nodoc:
msg = build_message(message, "the flash is empty")
assert_block(msg) { @response.has_flash_with_contents? }
end
def assert_flash_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, @response.flash[key])
assert_block(msg) { expected == @response.flash[key] }
end
# ensure our redirection url is an exact match
def assert_redirect_url(url=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> is not the redirected location <?>", url, @response.redirect_url)
assert_block(msg) { @response.redirect_url == url }
end
# ensure our redirection url matches a pattern
def assert_redirect_url_match(pattern=nil, message=nil) #:nodoc:
assert_redirect(message)
msg = build_message(message, "<?> was not found in the location: <?>", pattern, @response.redirect_url)
assert_block(msg) { @response.redirect_url_match?(pattern) }
end
# -- template assertions ------------------------------------------------
# ensure that a template object with the given name exists
def assert_template_has(key=nil, message=nil) #:nodoc:
msg = build_message(message, "<?> is not a template object", key )
assert_block(msg) { @response.has_template_object?(key) }
end
# ensure that a template object with the given name does not exist
def assert_template_has_no(key=nil,message=nil) #:nodoc:
msg = build_message(message, "<?> is a template object <?>", key, @response.template_objects[key])
assert_block(msg) { !@response.has_template_object?(key) }
end
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
def assert_template_equal(expected = nil, key = nil, message = nil) #:nodoc:
msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, @response.template.assigns[key.to_s])
assert_block(msg) { expected == @response.template.assigns[key.to_s] }
end
alias_method :assert_assigned_equal, :assert_template_equal
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
# This will only work if the template rendered a valid XML document.
def assert_template_xpath_match(expression=nil, expected=nil, message=nil) #:nodoc:
xml, matches = REXML::Document.new(@response.body), []
xml.elements.each(expression) { |e| matches << e.text }
if matches.empty? then
msg = build_message(message, "<?> not found in document", expression)
flunk(msg)
return
elsif matches.length < 2 then
matches = matches.first
end
msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
assert_block(msg) { matches == expected }
end
# Assert the template object with the given name is an Active Record descendant and is valid.
def assert_valid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
assert_block(msg) { record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and is invalid.
def assert_invalid_record(key = nil, message = nil) #:nodoc:
record = find_record_in_template(key)
msg = build_message(message, "Active Record is valid)")
assert_block(msg) { !record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
def assert_valid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| !record.errors.invalid?(col) }
msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
def assert_invalid_column_on_record(key = nil, columns = "", message = nil) #:nodoc:
record = find_record_in_template(key)
record.send(:validate)
cols = glue_columns(columns)
cols.delete_if { |col| record.errors.invalid?(col) }
msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
private
def glue_columns(columns)
cols = []
cols << columns if columns.class == String
cols += columns if columns.class == Array
cols
end
def find_record_in_template(key = nil)
assert_template_has(key)
record = @response.template_objects[key]
assert_not_nil(record)
assert_kind_of ActiveRecord::Base, record
return record
end
end
end
end

View file

@ -1,65 +0,0 @@
module ActionController #:nodoc:
module Dependencies #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# Deprecated module. The responsibility of loading dependencies belong with Active Support now.
module ClassMethods #:nodoc:
# Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
# backend for modelling entity classes.
def model(*models)
require_dependencies(:model, models)
depend_on(:model, models)
end
deprecate :model
# Specifies a variable number of services that this controller depends on. Services are normally singletons or factories, like
# Action Mailer service or a Payment Gateway service.
def service(*services)
require_dependencies(:service, services)
depend_on(:service, services)
end
deprecate :service
# Specifies a variable number of observers that are to govern when this controller is handling actions. The observers will
# automatically have .instance called on them to make them active on assignment.
def observer(*observers)
require_dependencies(:observer, observers)
depend_on(:observer, observers)
instantiate_observers(observers)
end
deprecate :observer
# Returns an array of symbols that specify the dependencies on a given layer. For the example at the top, calling
# <tt>ApplicationController.dependencies_on(:model)</tt> would return <tt>[:account, :company, :person, :project, :category]</tt>
def dependencies_on(layer)
read_inheritable_attribute("#{layer}_dependencies")
end
deprecate :dependencies_on
def depend_on(layer, dependencies) #:nodoc:
write_inheritable_array("#{layer}_dependencies", dependencies)
end
deprecate :depend_on
private
def instantiate_observers(observers)
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
end
def require_dependencies(layer, dependencies)
dependencies.flatten.each do |dependency|
begin
require_dependency(dependency.to_s)
rescue LoadError => e
raise LoadError.new("Missing #{layer} #{dependency}.rb").copy_blame!(e)
rescue Exception => exception # error from loaded file
exception.blame_file! "=> #{layer} #{dependency}.rb"
raise
end
end
end
end
end
end

View file

@ -1,17 +0,0 @@
module ActionController
class Base
protected
# Deprecated in favor of calling redirect_to directly with the path.
def redirect_to_path(path) #:nodoc:
redirect_to(path)
end
# Deprecated in favor of calling redirect_to directly with the url. If the resource has moved permanently, it's possible to pass
# true as the second parameter and the browser will get "301 Moved Permanently" instead of "302 Found". This can also be done through
# just setting the headers["Status"] to "301 Moved Permanently" before using the redirect_to.
def redirect_to_url(url, permanently = false) #:nodoc:
headers["Status"] = "301 Moved Permanently" if permanently
redirect_to(url)
end
end
end

View file

@ -1,34 +0,0 @@
module ActionController
class AbstractRequest
# Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def post_format
case content_type.to_s
when 'application/xml'
:xml
when 'application/x-yaml'
:yaml
else
:url_encoded
end
end
# Is this a POST request formatted as XML or YAML?
def formatted_post?
post? && (post_format == :yaml || post_format == :xml)
end
# Is this a POST request formatted as XML?
def xml_post?
post? && post_format == :xml
end
# Is this a POST request formatted as YAML?
def yaml_post?
post? && post_format == :yaml
end
end
end

View file

@ -0,0 +1,195 @@
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
class << self
# Backward-compatible class method takes CGI-specific args. Deprecated
# in favor of Dispatcher.new(output, request, response).dispatch.
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
new(output).dispatch_cgi(cgi, session_options)
end
# Declare a block to be called before each dispatch.
# Run in the order declared.
def before_dispatch(*method_names, &block)
callbacks[:before].concat method_names
callbacks[:before] << block if block_given?
end
# Declare a block to be called after each dispatch.
# Run in reverse of the order declared.
def after_dispatch(*method_names, &block)
callbacks[:after].concat method_names
callbacks[:after] << block if block_given?
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)
# Already registered: update the existing callback
if identifier
if callback = callbacks[:prepare].assoc(identifier)
callback[1] = block
else
callbacks[:prepare] << [identifier, block]
end
else
callbacks[:prepare] << block
end
end
# If the block raises, send status code as a last-ditch response.
def failsafe_response(fallback_output, status, originating_exception = nil)
yield
rescue Exception => exception
begin
log_failsafe_exception(status, originating_exception || exception)
body = failsafe_response_body(status)
fallback_output.write "Status: #{status}\r\nContent-Type: text/html\r\n\r\n#{body}"
nil
rescue Exception => failsafe_error # Logger or IO errors
$stderr.puts "Error during failsafe response: #{failsafe_error}"
$stderr.puts "(originally #{originating_exception})" if originating_exception
end
end
private
def failsafe_response_body(status)
error_path = "#{error_file_path}/#{status.to_s[0..3]}.html"
if File.exist?(error_path)
File.read(error_path)
else
"<html><body><h1>#{status}</h1></body></html>"
end
end
def log_failsafe_exception(status, exception)
message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: #{status}\n"
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
failsafe_logger.fatal message
end
def failsafe_logger
if defined?(::RAILS_DEFAULT_LOGGER) && !::RAILS_DEFAULT_LOGGER.nil?
::RAILS_DEFAULT_LOGGER
else
Logger.new($stderr)
end
end
end
cattr_accessor :error_file_path
self.error_file_path = "#{::RAILS_ROOT}/public" if defined? ::RAILS_ROOT
cattr_accessor :callbacks
self.callbacks = Hash.new { |h, k| h[k] = [] }
cattr_accessor :unprepared
self.unprepared = true
before_dispatch :reload_application
before_dispatch :prepare_application
after_dispatch :flush_logger
after_dispatch :cleanup_application
if defined? ActiveRecord
to_prepare :activerecord_instantiate_observers do
ActiveRecord::Base.instantiate_observers
end
end
def initialize(output, request = nil, response = nil)
@output, @request, @response = output, request, response
end
def dispatch
run_callbacks :before
handle_request
rescue Exception => exception
failsafe_rescue exception
ensure
run_callbacks :after, :reverse_each
end
def dispatch_cgi(cgi, session_options)
if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new }
@request = CgiRequest.new(cgi, session_options)
@response = CgiResponse.new(cgi)
dispatch
end
rescue Exception => exception
failsafe_rescue exception
end
def reload_application
if Dependencies.load?
Routing::Routes.reload
self.unprepared = true
end
end
def prepare_application(force = false)
begin
require_dependency 'application' unless defined?(::ApplicationController)
rescue LoadError => error
raise unless error.message =~ /application\.rb/
end
ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)
if unprepared || force
run_callbacks :prepare
self.unprepared = false
end
end
# Cleanup the application by clearing out loaded classes so they can
# be reloaded on the next request without restarting the server.
def cleanup_application(force = false)
if Dependencies.load? || force
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end
end
def flush_logger
RAILS_DEFAULT_LOGGER.flush if defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:flush)
end
protected
def handle_request
@controller = Routing::Routes.recognize(@request)
@controller.process(@request, @response).out(@output)
end
def run_callbacks(kind, enumerator = :each)
callbacks[kind].send!(enumerator) do |callback|
case callback
when Proc; callback.call(self)
when String, Symbol; send!(callback)
when Array; callback[1].call(self)
else raise ArgumentError, "Unrecognized callback #{callback.inspect}"
end
end
end
def failsafe_rescue(exception)
self.class.failsafe_response(@output, '500 Internal Server Error', exception) do
if @controller ||= defined?(::ApplicationController) ? ::ApplicationController : Base
@controller.process_with_exception(@request, @response, exception).out(@output)
else
raise exception
end
end
end
end
end

View file

@ -1,11 +1,13 @@
module ActionController #:nodoc:
module Filters #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.send(:include, ActionController::Filters::InstanceMethods)
base.class_eval do
extend ClassMethods
include ActionController::Filters::InstanceMethods
end
end
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed. Filters have access to the request, response, and all the instance
# variables set by other filters in the chain or by the action (in the case of after filters).
@ -34,7 +36,7 @@ module ActionController #:nodoc:
# end
#
# Now any actions performed on the BankController will have the audit method called before. On the VaultController,
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# first the audit method is called, then the verify_credentials method. If the audit method renders or redirects, then
# verify_credentials and the intended action are never called.
#
# == Filter types
@ -63,7 +65,7 @@ module ActionController #:nodoc:
# Or just as a quick test. It works like this:
#
# class WeblogController < ActionController::Base
# before_filter { |controller| false if controller.params["stop_action"] }
# before_filter { |controller| head(400) if controller.params["stop_action"] }
# end
#
# As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
@ -88,7 +90,7 @@ module ActionController #:nodoc:
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
#
# The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
# <tt>:verify_open_shop</tt>. So if either of the ensure filters renders or redirects, we'll never get around to see if the shop
# is open or not.
#
# You may pass multiple filter arguments of each type as well as a filter block.
@ -140,23 +142,20 @@ module ActionController #:nodoc:
# around_filter Authorizer.new
#
# class Authorizer
# # This will run before the action. Returning false aborts the action.
# # This will run before the action. Redirecting aborts the action.
# def before(controller)
# if user.authorized?
# return true
# else
# redirect_to login_url
# return false
# unless user.authorized?
# redirect_to(login_url)
# end
# end
#
# # This will run after the action if and only if before returned true.
# # This will run after the action if and only if before did not render or redirect.
# def after(controller)
# end
# end
#
# If the filter has before and after methods, the before method will be
# called before the action. If before returns false, the filter chain is
# called before the action. If before renders or redirects, the filter chain is
# halted and after will not be run. See Filter Chain Halting below for
# an example.
#
@ -216,8 +215,8 @@ module ActionController #:nodoc:
# <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request
# before a controller action is run. This is useful, for example, to deny
# access to unauthenticated users or to redirect from http to https.
# Simply return false from the filter or call render or redirect.
# After filters will not be executed if the filter chain is halted.
# Simply call render or redirect. After filters will not be executed if the filter
# chain is halted.
#
# Around filters halt the request unless the action block is called.
# Given these filters
@ -242,9 +241,9 @@ module ActionController #:nodoc:
# #after (actual filter code is run, unless the around filter does not yield)
#
# If #around returns before yielding, #after will still not be run. The #before
# filter and controller action will not be run. If #before returns false,
# filter and controller action will not be run. If #before renders or redirects,
# the second half of #around and will still run but #after and the
# action will not. If #around does not yield, #after will not be run.
# action will not. If #around fails to yield, #after will not be run.
module ClassMethods
# The passed <tt>filters</tt> will be appended to the filter_chain and
# will execute before the action on this controller is performed.
@ -439,8 +438,9 @@ module ActionController #:nodoc:
def run(controller)
# only filters returning false are halted.
if false == @filter.call(controller)
controller.send :halt_filter_chain, @filter, :returned_false
@filter.call(controller)
if controller.send!(:performed?)
controller.send!(:halt_filter_chain, @filter, :rendered_or_redirected)
end
end
@ -466,7 +466,7 @@ module ActionController #:nodoc:
class SymbolFilter < Filter #:nodoc:
def call(controller, &block)
controller.send(@filter, &block)
controller.send!(@filter, &block)
end
end
@ -601,7 +601,7 @@ module ActionController #:nodoc:
def extract_conditions(*filters, &block) #:nodoc:
filters.flatten!
conditions = filters.last.is_a?(Hash) ? filters.pop : {}
conditions = filters.extract_options!
filters << block if block_given?
return filters, conditions
end
@ -655,8 +655,10 @@ module ActionController #:nodoc:
def proxy_before_and_after_filter(filter) #:nodoc:
return filter unless filter_responds_to_before_and_after(filter)
Proc.new do |controller, action|
if filter.before(controller) == false
controller.send :halt_filter_chain, filter, :returned_false
filter.before(controller)
if controller.send!(:performed?)
controller.send!(:halt_filter_chain, filter, :rendered_or_redirected)
else
begin
action.call
@ -673,7 +675,6 @@ module ActionController #:nodoc:
base.class_eval do
alias_method_chain :perform_action, :filters
alias_method_chain :process, :filters
alias_method_chain :process_cleanup, :filters
end
end
@ -709,6 +710,7 @@ module ActionController #:nodoc:
while chain[index]
filter, index = skip_excluded_filters(chain, index)
break unless filter # end of call chain reached
case filter.type
when :before
filter.run(self) # invoke before filter
@ -716,25 +718,31 @@ module ActionController #:nodoc:
break if @before_filter_chain_aborted
when :around
yielded = false
filter.call(self) do
yielded = true
# all remaining before and around filters will be run in this call
index = call_filters(chain, index.next, nesting.next)
end
halt_filter_chain(filter, :did_not_yield) unless yielded
break
else
break # no before or around filters left
end
end
index
end
def run_after_filters(chain, index)
seen_after_filter = false
while chain[index]
filter, index = skip_excluded_filters(chain, index)
break unless filter # end of call chain reached
case filter.type
when :after
seen_after_filter = true
@ -743,23 +751,16 @@ module ActionController #:nodoc:
# implementation error or someone has mucked with the filter chain
raise ActionControllerError, "filter #{filter.inspect} was in the wrong place!" if seen_after_filter
end
index = index.next
end
index.next
end
def halt_filter_chain(filter, reason)
@before_filter_chain_aborted = true
logger.info "Filter chain halted as [#{filter.inspect}] #{reason}." if logger
false
end
def process_cleanup_with_filters
if @before_filter_chain_aborted
close_session
else
process_cleanup_without_filters
end
end
end
end

View file

@ -1,8 +1,8 @@
module ActionController #:nodoc:
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
# that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can then expose
# the flash to its template. Actually, that exposure is automatically done. Example:
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done. Example:
#
# class WeblogController < ActionController::Base
# def create
@ -16,18 +16,17 @@ module ActionController #:nodoc:
# end
# end
#
# display.rhtml
# display.erb
# <% if flash[:notice] %><div class="notice"><%= flash[:notice] %></div><% end %>
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
# as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as
# many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
module Flash
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
include InstanceMethods
alias_method_chain :assign_shortcuts, :flash
alias_method_chain :process_cleanup, :flash
alias_method_chain :reset_session, :flash
@ -63,7 +62,7 @@ module ActionController #:nodoc:
end
def update(h) #:nodoc:
h.keys.each{ |k| discard(k) }
h.keys.each { |k| keep(k) }
super
end
@ -85,7 +84,7 @@ module ActionController #:nodoc:
#
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
def now
FlashNow.new self
FlashNow.new(self)
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
@ -96,10 +95,10 @@ module ActionController #:nodoc:
use(k, false)
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
#
# flash.keep # keep entire flash available for the next action
# flash.discard(:warning) # discard the "warning" entry (it'll still be available for the current action)
# flash.discard # discard the entire flash at the end of the current action
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
def discard(k = nil)
use(k)
end
@ -117,7 +116,8 @@ module ActionController #:nodoc:
end
end
(@used.keys - keys).each{|k| @used.delete k } # clean up after keys that could have been left over by calling reject! or shift on the flash
# clean up after keys that could have been left over by calling reject! or shift on the flash
(@used.keys - keys).each{ |k| @used.delete(k) }
end
private
@ -130,29 +130,19 @@ module ActionController #:nodoc:
unless k.nil?
@used[k] = v
else
keys.each{|key| use key, v }
keys.each{ |key| use(key, v) }
end
end
end
module InstanceMethods #:nodoc:
def assign_shortcuts_with_flash(request, response) #:nodoc:
assign_shortcuts_without_flash(request, response)
flash(:refresh)
end
protected
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash)
flash(:refresh)
end
def process_cleanup_with_flash
flash.sweep if @_session
process_cleanup_without_flash
end
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash)
flash(:refresh)
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.
# Note that if sessions are disabled only flash.now will work.
@ -172,10 +162,15 @@ module ActionController #:nodoc:
@_flash
end
# deprecated. use <tt>flash.keep</tt> instead
def keep_flash #:doc:
ActiveSupport::Deprecation.warn 'keep_flash is deprecated; use flash.keep instead.', caller
flash.keep
private
def assign_shortcuts_with_flash(request, response) #:nodoc:
assign_shortcuts_without_flash(request, response)
flash(:refresh)
end
def process_cleanup_with_flash
flash.sweep if @_session
process_cleanup_without_flash
end
end
end

View file

@ -1,5 +1,8 @@
# FIXME: helper { ... } is broken on Ruby 1.9
module ActionController #:nodoc:
module Helpers #:nodoc:
HELPERS_DIR = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers")
def self.included(base)
# Initialize the base module to aggregate its helpers.
base.class_inheritable_accessor :master_helper_module
@ -16,64 +19,120 @@ module ActionController #:nodoc:
end
end
# The template helpers serve to relieve the templates from including the same inline code again and again. It's a
# set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
# Active Records (ActiveRecordHelper) that's available to all templates by default.
# The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+,
# +numbers+ and +ActiveRecord+ objects, to name a few. These helpers are available to all templates
# by default.
#
# It's also really easy to make your own helpers and it's much encouraged to keep the template files free
# from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
# (often the common helpers) as they're used by the specific application.
# In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will
# include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically
# include <tt>MyHelper</tt>.
#
# module MyHelper
# def hello_world() "hello world" end
# Additional helpers can be specified using the +helper+ class method in <tt>ActionController::Base</tt> or any
# controller which inherits from it.
#
# ==== Examples
# The +to_s+ method from the +Time+ class can be wrapped in a helper method to display a custom message if
# the Time object is blank:
#
# module FormattedTimeHelper
# def format_time(time, format=:long, blank_message="&nbsp;")
# time.blank? ? blank_message : time.to_s(format)
# end
# end
#
# MyHelper can now be included in a controller, like this:
#
# class MyController < ActionController::Base
# helper :my_helper
#
# +FormattedTimeHelper+ can now be included in a controller, using the +helper+ class method:
#
# class EventsController < ActionController::Base
# helper FormattedTimeHelper
# def index
# @events = Event.find(:all)
# end
# end
#
# ...and, same as above, used in any template rendered from MyController, like this:
#
# Let's hear what the helper has to say: <tt><%= hello_world %></tt>
#
# Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
#
# <% @events.each do |event| -%>
# <p>
# <% format_time(event.time, :short, "N/A") %> | <%= event.name %>
# </p>
# <% end -%>
#
# Finally, assuming we have two event instances, one which has a time and one which does not,
# the output might look like this:
#
# 23 Aug 11:30 | Carolina Railhawks Soccer Match
# N/A | Carolina Railhaws Training Workshop
#
module ClassMethods
# 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
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
# available to the templates.
def add_template_helper(helper_module) #:nodoc:
master_helper_module.send(:include, helper_module)
master_helper_module.module_eval { include helper_module }
end
# Declare a helper:
# helper :foo
# requires 'foo_helper' and includes FooHelper in the template class.
# helper FooHelper
# includes FooHelper in the template class.
# helper { def foo() "#{bar} is the very best" end }
# evaluates the block in the template class, adding method #foo.
# 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 from
# <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 }
# does all three.
#
def helper(*args, &block)
args.flatten.each do |arg|
case arg
when Module
add_template_helper(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).to_a[1]
msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}"
raise LoadError.new(msg).copy_blame!(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
add_template_helper(class_name.constantize)
else
raise ArgumentError, 'helper expects String, Symbol, or Module argument'
raise ArgumentError, "helper expects String, Symbol, or Module argument (was: #{args.inspect})"
end
end
@ -81,10 +140,14 @@ module ActionController #:nodoc:
master_helper_module.module_eval(&block) if block_given?
end
# Declare a controller method as a helper. For example,
# helper_method :link_to
# def link_to(name, options) ... end
# makes the link_to controller method available in the view.
# 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
# def current_user
# @current_user ||= User.find(session[:user])
# end
# end
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
@ -95,37 +158,47 @@ module ActionController #:nodoc:
end
end
# Declare a controller attribute as a helper. For example,
# 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
# makes the name and name= controller methods available in the view.
# The is a convenience wrapper for helper_method.
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
private
private
def default_helper_module!
module_name = name.sub(/Controller$|$/, 'Helper')
module_path = module_name.split('::').map { |m| m.underscore }.join('/')
require_dependency module_path
helper module_name.constantize
rescue LoadError
logger.debug("#{name}: missing default helper path #{module_path}") if logger
rescue NameError
logger.debug("#{name}: missing default helper module #{module_name}") if logger
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
def inherited_with_helper(child)
inherited_without_helper(child)
begin
child.master_helper_module = Module.new
child.master_helper_module.send :include, master_helper_module
child.send :default_helper_module!
child.master_helper_module.send! :include, master_helper_module
child.send! :default_helper_module!
rescue MissingSourceFile => e
raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
end
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

View file

@ -0,0 +1,126 @@
require 'base64'
module ActionController
module HttpAuthentication
# Makes it dead easy to do HTTP Basic authentication.
#
# Simple Basic example:
#
# class PostsController < ApplicationController
# USER_NAME, PASSWORD = "dhh", "secret"
#
# before_filter :authenticate, :except => [ :index ]
#
# def index
# render :text => "Everyone can see me!"
# end
#
# def edit
# render :text => "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_basic do |user_name, password|
# user_name == USER_NAME && password == PASSWORD
# end
# end
# end
#
#
# Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
# before_filter :set_account, :authenticate
#
# protected
# def set_account
# @account = Account.find_by_url_name(request.subdomains.first)
# end
#
# def authenticate
# case request.format
# when Mime::XML, Mime::ATOM
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
# @current_user = user
# else
# request_http_basic_authentication
# end
# else
# if session_authenticated?
# @current_user = @account.users.find(session[:authenticated][:user_id])
# else
# redirect_to(login_url) and return false
# end
# end
# end
# end
#
#
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
# get(
# "/notes/1.xml", nil,
# :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
# )
#
# assert_equal 200, status
# end
#
#
# On shared hosts, Apache sometimes doesn't pass authentication headers to
# FCGI instances. If your environment matches this description and you cannot
# authenticate, try this rule in public/.htaccess (replace the plain one):
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Basic
extend self
module ControllerMethods
def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure)
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm)
end
def authenticate_with_http_basic(&login_procedure)
HttpAuthentication::Basic.authenticate(self, &login_procedure)
end
def request_http_basic_authentication(realm = "Application")
HttpAuthentication::Basic.authentication_request(self, realm)
end
end
def authenticate(controller, &login_procedure)
unless authorization(controller.request).blank?
login_procedure.call(*user_name_and_password(controller.request))
end
end
def user_name_and_password(request)
decode_credentials(request).split(/:/, 2)
end
def authorization(request)
request.env['HTTP_AUTHORIZATION'] ||
request.env['X-HTTP_AUTHORIZATION'] ||
request.env['X_HTTP_AUTHORIZATION'] ||
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
end
def decode_credentials(request)
Base64.decode64(authorization(request).split.last || '')
end
def encode_credentials(user_name, password)
"Basic #{Base64.encode64("#{user_name}:#{password}")}"
end
def authentication_request(controller, realm)
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
controller.send! :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end
end
end
end

View file

@ -51,7 +51,10 @@ module ActionController
# A reference to the response instance used by the last request.
attr_reader :response
# Create an initialize a new Session instance.
# A running counter of the number of requests processed.
attr_accessor :request_count
# Create and initialize a new +Session+ instance.
def initialize
reset!
end
@ -67,19 +70,20 @@ module ActionController
@https = false
@cookies = {}
@controller = @request = @response = nil
@request_count = 0
self.host = "www.example.com"
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,*/*;q=0.5"
unless @named_routes_configured
unless defined? @named_routes_configured
# install the named routes in this session instance.
klass = class<<self; self; end
Routing::Routes.named_routes.install(klass)
Routing::Routes.install_helpers(klass)
# the helpers are made protected by default--we make them public for
# easier access during testing and troubleshooting.
klass.send(:public, *Routing::Routes.named_routes.helpers)
klass.module_eval { public *Routing::Routes.named_routes.helpers }
@named_routes_configured = true
end
end
@ -117,22 +121,38 @@ module ActionController
status
end
# Performs a GET request, 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 get_via_redirect(path, args={})
get path, args
# 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 POST request, following any subsequent redirect. This is
# vulnerable to infinite loops, the same as #get_via_redirect.
def post_via_redirect(path, args={})
post path, args
follow_redirect! while redirect?
status
# 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.
@ -148,27 +168,27 @@ module ActionController
#
# You can also perform POST, PUT, DELETE, and HEAD requests with #post,
# #put, #delete, and #head.
def get(path, parameters=nil, headers=nil)
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)
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)
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)
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)
def head(path, parameters = nil, headers = nil)
process :head, path, parameters, headers
end
@ -179,19 +199,10 @@ module ActionController
# 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.
#
# This method used to omit the request_method parameter, assuming it
# was :post. This was deprecated in Rails 1.2.4. Always pass the request
# method as the first argument.
def xml_http_request(request_method, path, parameters = nil, headers = nil)
unless request_method.is_a?(Symbol)
ActiveSupport::Deprecation.warn 'xml_http_request now takes the request_method (:get, :post, etc.) as the first argument. It used to assume :post, so add the :post argument to your existing method calls to silence this warning.'
request_method, path, parameters, headers = :post, request_method, path, parameters
end
headers ||= {}
headers['X-Requested-With'] = 'XMLHttpRequest'
headers['Accept'] = 'text/javascript, text/html, application/xml, text/xml, */*'
headers['Accept'] ||= 'text/javascript, text/html, application/xml, text/xml, */*'
process(request_method, path, parameters, headers)
end
@ -204,15 +215,16 @@ module ActionController
end
private
class MockCGI < CGI #:nodoc:
class StubCGI < CGI #:nodoc:
attr_accessor :stdinput, :stdoutput, :env_table
def initialize(env, input=nil)
def initialize(env, stdinput = nil)
self.env_table = env
self.stdinput = StringIO.new(input || "")
self.stdoutput = StringIO.new
super()
super
@stdinput = stdinput.is_a?(IO) ? stdinput : StringIO.new(stdinput || '')
end
end
@ -226,7 +238,7 @@ module ActionController
end
# Performs the actual request.
def process(method, path, parameters=nil, headers=nil)
def process(method, path, parameters = nil, headers = nil)
data = requestify(parameters)
path = interpret_uri(path) if path =~ %r{://}
path = "/#{path}" unless path[0] == ?/
@ -258,14 +270,15 @@ module ActionController
end
unless ActionController::Base.respond_to?(:clear_last_instantiation!)
ActionController::Base.send(:include, ControllerCapture)
ActionController::Base.module_eval { include ControllerCapture }
end
ActionController::Base.clear_last_instantiation!
cgi = MockCGI.new(env, data)
cgi = StubCGI.new(env, data)
Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput)
@result = cgi.stdoutput.string
@request_count += 1
@controller = ActionController::Base.last_instantiation
@request = @controller.request
@ -294,7 +307,7 @@ module ActionController
end
(@headers['set-cookie'] || [] ).each do |string|
name, value = string.match(/^(.*?)=(.*?);/)[1,2]
name, value = string.match(/^([^=]*)=([^;]*);/)[1,2]
@cookies[name] = value
end
@ -310,14 +323,14 @@ module ActionController
end
end
# Get a temporarly URL writer object
# Get a temporary URL writer object
def generic_url_rewriter
cgi = MockCGI.new('REQUEST_METHOD' => "GET",
cgi = StubCGI.new('REQUEST_METHOD' => "GET",
'QUERY_STRING' => "",
"REQUEST_URI" => "/",
"HTTP_HOST" => host,
"SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off")
"HTTPS" => https? ? "on" : "off")
ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {})
end
@ -339,7 +352,6 @@ module ActionController
"#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}"
end
end
end
# A module used to extend ActionController::Base, so that integration tests
@ -368,6 +380,76 @@ module ActionController
end
end
end
module Runner
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
@integration_session = open_session
end
%w(get post put head delete cookies assigns
xml_http_request get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args|
reset! unless @integration_session
# reset the html_document variable, but only for new get/post calls
@html_document = nil unless %w(cookies assigns).include?(method)
returning @integration_session.send!(method, *args) do
copy_session_variables!
end
end
end
# Open a new session instance. If a block is given, the new session is
# yielded to the block before being returned.
#
# session = open_session do |sess|
# sess.extend(CustomAssertions)
# end
#
# 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
session = Integration::Session.new
# delegate the fixture accessors back to the test instance
extras = Module.new { attr_accessor :delegate, :test_result }
if self.class.respond_to?(:fixture_table_names)
self.class.fixture_table_names.each do |table_name|
name = table_name.tr(".", "_")
next unless respond_to?(name)
extras.send!(:define_method, name) { |*args| delegate.send(name, *args) }
end
end
# delegate add_assertion to the test case
extras.send!(:define_method, :add_assertion) { test_result.add_assertion }
session.extend(extras)
session.delegate = self
session.test_result = @_result
yield session if block_given?
session
end
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
return unless @integration_session
%w(controller response request).each do |var|
instance_variable_set("@#{var}", @integration_session.send!(var))
end
end
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
reset! unless @integration_session
returning @integration_session.send!(sym, *args, &block) do
copy_session_variables!
end
end
end
end
# An IntegrationTest is one that spans multiple controllers and actions,
@ -446,6 +528,8 @@ module ActionController
# end
# end
class IntegrationTest < Test::Unit::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
@ -493,70 +577,5 @@ module ActionController
superclass.use_instantiated_fixtures
end
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
@integration_session = open_session
end
%w(get post put head delete cookies assigns xml_http_request).each do |method|
define_method(method) do |*args|
reset! unless @integration_session
# reset the html_document variable, but only for new get/post calls
@html_document = nil unless %w(cookies assigns).include?(method)
returning @integration_session.send(method, *args) do
copy_session_variables!
end
end
end
# Open a new session instance. If a block is given, the new session is
# yielded to the block before being returned.
#
# session = open_session do |sess|
# sess.extend(CustomAssertions)
# end
#
# 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
session = Integration::Session.new
# delegate the fixture accessors back to the test instance
extras = Module.new { attr_accessor :delegate, :test_result }
self.class.fixture_table_names.each do |table_name|
name = table_name.tr(".", "_")
next unless respond_to?(name)
extras.send(:define_method, name) { |*args| delegate.send(name, *args) }
end
# delegate add_assertion to the test case
extras.send(:define_method, :add_assertion) { test_result.add_assertion }
session.extend(extras)
session.delegate = self
session.test_result = @_result
yield session if block_given?
session
end
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
return unless @integration_session
%w(controller response request).each do |var|
instance_variable_set("@#{var}", @integration_session.send(var))
end
end
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
reset! unless @integration_session
returning @integration_session.send(sym, *args, &block) do
copy_session_variables!
end
end
end
end

View file

@ -64,13 +64,13 @@ module ActionController #:nodoc:
#
# If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically
# set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named
# <tt>app/views/layouts/weblog.rhtml</tt> or <tt>app/views/layouts/weblog.rxml</tt> exists then it will be automatically set as
# the layout for your WeblogController. You can create a layout with the name <tt>application.rhtml</tt> or <tt>application.rxml</tt>
# <tt>app/views/layouts/weblog.erb</tt> or <tt>app/views/layouts/weblog.builder</tt> exists then it will be automatically set as
# the layout for your WeblogController. You can create a layout with the name <tt>application.erb</tt> or <tt>application.builder</tt>
# and this will be set as the default controller if there is no layout with the same name as the current controller and there is
# no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout.
# assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.rhtml</tt>.
# assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.erb</tt>.
# Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set.
# Explicitly setting the layout in a parent class, though, will not override the child class's layout assignement if the child
# Explicitly setting the layout in a parent class, though, will not override the child class's layout assignment if the child
# class has a layout with the same name.
#
# == Inheritance for layouts
@ -124,7 +124,8 @@ module ActionController #:nodoc:
# class WeblogController < ActionController::Base
# layout "weblog_standard"
#
# If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
# If no directory is specified for the template name, the template will by default be looked for in +app/views/layouts/+.
# Otherwise, it will be looked up relative to the template root.
#
# == Conditional layouts
#
@ -165,30 +166,39 @@ module ActionController #:nodoc:
# variable. The preferred notation now is to use <tt>yield</tt>, as documented above.
module ClassMethods
# If a layout is specified, all rendered actions will have their result rendered
# when the layout<tt>yield</tt>'s. This layout can itself depend on instance variables assigned during action
# when the layout <tt>yield</tt>s. This layout can itself depend on instance variables assigned during action
# performance and have access to them as any normal template would.
def layout(template_name, conditions = {})
def layout(template_name, conditions = {}, auto = false)
add_layout_conditions(conditions)
write_inheritable_attribute "layout", template_name
write_inheritable_attribute "auto_layout", auto
end
def layout_conditions #:nodoc:
@layout_conditions ||= read_inheritable_attribute("layout_conditions")
end
def default_layout #:nodoc:
@default_layout ||= read_inheritable_attribute("layout")
def default_layout(format) #:nodoc:
layout = read_inheritable_attribute("layout")
return layout unless read_inheritable_attribute("auto_layout")
@default_layout ||= {}
@default_layout[format] ||= default_layout_with_format(format, layout)
@default_layout[format]
end
def layout_list #:nodoc:
view_paths.collect do |path|
Dir["#{path}/layouts/**/*"]
end.flatten
end
private
def inherited_with_layout(child)
inherited_without_layout(child)
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
child.layout(layout_match) unless layout_list.grep(%r{layouts/#{layout_match}\.[a-z][0-9a-z]*$}).empty?
end
def layout_list
Dir.glob("#{template_root}/layouts/**/*")
unless child.name.blank?
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
child.layout(layout_match, {}, true) unless child.layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty?
end
end
def add_layout_conditions(conditions)
@ -204,6 +214,15 @@ module ActionController #:nodoc:
h[dirname] = File.directory? dirname
end
end
def default_layout_with_format(format, layout)
list = layout_list
if list.grep(%r{layouts/#{layout}\.#{format}(\.[a-z][0-9a-z]*)+$}).empty?
(!list.grep(%r{layouts/#{layout}\.([a-z][0-9a-z]*)+$}).empty? && format == :html) ? layout : nil
else
layout
end
end
end
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
@ -211,11 +230,10 @@ module ActionController #:nodoc:
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
# weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
def active_layout(passed_layout = nil)
layout = passed_layout || self.class.default_layout
layout = passed_layout || self.class.default_layout(response.template.template_format)
active_layout = case layout
when String then layout
when Symbol then send(layout)
when Symbol then send!(layout)
when Proc then layout.call(self)
end
@ -231,34 +249,30 @@ module ActionController #:nodoc:
end
end
def render_with_a_layout(options = nil, deprecated_status = nil, deprecated_layout = nil, &block) #:nodoc:
template_with_options = options.is_a?(Hash)
protected
def render_with_a_layout(options = nil, &block) #:nodoc:
template_with_options = options.is_a?(Hash)
if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options))
assert_existence_of_template_file(layout)
if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options, deprecated_layout))
assert_existence_of_template_file(layout)
options = options.merge :layout => false if template_with_options
logger.info("Rendering template within #{layout}") if logger
options = options.merge :layout => false if template_with_options
logger.info("Rendering #{options} within #{layout}") if logger
if template_with_options
content_for_layout = render_with_no_layout(options, &block)
deprecated_status = options[:status] || deprecated_status
erase_render_results
add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout)
response.layout = layout
status = template_with_options ? options[:status] : nil
render_for_text(@template.render_file(layout, true), status)
else
content_for_layout = render_with_no_layout(options, deprecated_status, &block)
render_with_no_layout(options, &block)
end
erase_render_results
add_variables_to_assigns
@template.instance_variable_set("@content_for_layout", content_for_layout)
response.layout = layout
render_text(@template.render_file(layout, true), deprecated_status)
else
render_with_no_layout(options, deprecated_status, &block)
end
end
private
def apply_layout?(template_with_options, options)
return false if options == :update
template_with_options ? candidate_for_layout?(options) : !template_exempt_from_layout?
@ -266,14 +280,12 @@ module ActionController #:nodoc:
def candidate_for_layout?(options)
(options.has_key?(:layout) && options[:layout] != false) ||
options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing).compact.empty? &&
!template_exempt_from_layout?(default_template_name(options[:action] || options[:template]))
options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing).compact.empty? &&
!template_exempt_from_layout?(options[:template] || default_template_name(options[:action]))
end
def pick_layout(template_with_options, options, deprecated_layout)
if deprecated_layout
deprecated_layout
elsif template_with_options
def pick_layout(template_with_options, options)
if template_with_options
case layout = options[:layout]
when FalseClass
nil
@ -305,9 +317,10 @@ module ActionController #:nodoc:
# Does a layout directory for this class exist?
# we cache this info in a class level hash
def layout_directory?(layout_name)
template_path = File.join(self.class.view_root, 'layouts', layout_name)
dirname = File.dirname(template_path)
self.class.send(:layout_directory_exists_cache)[dirname]
view_paths.find do |path|
next unless template_path = Dir[File.join(path, 'layouts', layout_name) + ".*"].first
self.class.send!(:layout_directory_exists_cache)[File.dirname(template_path)]
end
end
end
end

View file

@ -1,53 +0,0 @@
module ActionController
# Macros are class-level calls that add pre-defined actions to the controller based on the parameters passed in.
# Currently, they're used to bridge the JavaScript macros, like autocompletion and in-place editing, with the controller
# backing.
module Macros
module AutoComplete #:nodoc:
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Example:
#
# # Controller
# class BlogController < ApplicationController
# auto_complete_for :post, :title
# end
#
# # View
# <%= text_field_with_auto_complete :post, title %>
#
# By default, auto_complete_for limits the results to 10 entries,
# and sorts by the given field.
#
# auto_complete_for takes a third parameter, an options hash to
# the find method used to search for the records:
#
# auto_complete_for :post, :title, :limit => 15, :order => 'created_at DESC'
#
# For help on defining text input fields with autocompletion,
# see ActionView::Helpers::JavaScriptHelper.
#
# For more examples, see script.aculo.us:
# * http://script.aculo.us/demos/ajax/autocompleter
# * http://script.aculo.us/demos/ajax/autocompleter_customized
module ClassMethods
def auto_complete_for(object, method, options = {})
define_method("auto_complete_for_#{object}_#{method}") do
find_options = {
:conditions => [ "LOWER(#{method}) LIKE ?", '%' + params[object][method].downcase + '%' ],
:order => "#{method} ASC",
:limit => 10 }.merge!(options)
@items = object.to_s.camelize.constantize.find(:all, find_options)
render :inline => "<%= auto_complete_result @items, '#{method}' %>"
end
end
end
end
end
end

View file

@ -1,33 +0,0 @@
module ActionController
module Macros
module InPlaceEditing #:nodoc:
def self.included(base) #:nodoc:
base.extend(ClassMethods)
end
# DEPRECATION WARNING: This method will become a separate plugin when Rails 2.0 ships.
#
# Example:
#
# # Controller
# class BlogController < ApplicationController
# in_place_edit_for :post, :title
# end
#
# # View
# <%= in_place_editor_field :post, 'title' %>
#
# For help on defining an in place editor in the browser,
# see ActionView::Helpers::JavaScriptHelper.
module ClassMethods
def in_place_edit_for(object, attribute, options = {})
define_method("set_#{object}_#{attribute}") do
@item = object.to_s.camelize.constantize.find(params[:id])
@item.update_attribute(attribute, params[:value])
render :text => @item.send(attribute).to_s
end
end
end
end
end
end

View file

@ -1,7 +1,9 @@
module ActionController #:nodoc:
module MimeResponds #:nodoc:
def self.included(base)
base.send(:include, ActionController::MimeResponds::InstanceMethods)
base.module_eval do
include ActionController::MimeResponds::InstanceMethods
end
end
module InstanceMethods
@ -11,51 +13,51 @@ module ActionController #:nodoc:
# 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."
#
# 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
#
# 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
# (wants.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 persons company in the rendered XML, so you get something like this:
#
#
# 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>
# ...
@ -65,60 +67,54 @@ module ActionController #:nodoc:
# ...
# </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
#
# 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—a person. Then, in the action,
# we extract the company data from the request, find or create the company, and then create the new 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 note), but if you just go with the flow
#
# 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 note), 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(block.binding)
responder = Responder.new(self)
block.call(responder)
responder.respond
end
end
class Responder #:nodoc:
DEFAULT_BLOCKS = [:html, :js, :xml].inject({}) do |blocks, ext|
template_extension = (ext == :html ? '' : ".r#{ext}")
blocks.update ext => %(Proc.new { render :action => "\#{action_name}#{template_extension}", :content_type => Mime::#{ext.to_s.upcase} })
end
def initialize(block_binding)
@block_binding = block_binding
@mime_type_priority = eval(
"(params[:format] && Mime::EXTENSION_LOOKUP[params[:format]]) ? " +
"[ Mime::EXTENSION_LOOKUP[params[:format]] ] : request.accepts",
block_binding
)
def initialize(controller)
@controller = controller
@request = controller.request
@response = controller.response
@mime_type_priority = Array(Mime::Type.lookup_by_extension(@request.parameters[:format]) || @request.accepts)
@order = []
@responses = {}
@ -126,54 +122,47 @@ module ActionController #:nodoc:
def custom(mime_type, &block)
mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
@order << mime_type
if block_given?
@responses[mime_type] = Proc.new do
eval "response.content_type = '#{mime_type.to_s}'", @block_binding
block.call
end
else
if source = DEFAULT_BLOCKS[mime_type.to_sym]
@responses[mime_type] = eval(source, @block_binding)
else
raise ActionController::RenderError, "Expected a block but none was given for custom mime handler #{mime_type}"
end
@responses[mime_type] = Proc.new do
@response.template.template_format = mime_type.to_sym
@response.content_type = mime_type.to_s
block_given? ? block.call : @controller.send(:render, :action => @controller.action_name)
end
end
def any(*args, &block)
args.each { |type| send(type, &block) }
end
def method_missing(symbol, &block)
mime_constant = symbol.to_s.upcase
if Mime::SET.include?(Mime.const_get(mime_constant))
custom(Mime.const_get(mime_constant), &block)
else
super
end
end
def respond
for priority in @mime_type_priority
if priority == Mime::ALL
@responses[@order.first].call
return
else
if priority === @order
if @responses[priority]
@responses[priority].call
return # mime type match found, be happy and return
end
end
end
if @order.include?(Mime::ALL)
@responses[Mime::ALL].call
else
eval 'render(:nothing => true, :status => "406 Not Acceptable")', @block_binding
@controller.send :head, :not_acceptable
end
end
end

View file

@ -1,4 +1,8 @@
module Mime
SET = []
EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
# Encapsulates the notion of a mime type. Can be used at render time, for example, with:
#
# class PostsController < ActionController::Base
@ -20,7 +24,7 @@ module Mime
def initialize(order, name, q=nil)
@order = order
@name = name.strip
q ||= 0.0 if @name == "*/*" # default "*/*" to end of list
q ||= 0.0 if @name == Mime::ALL # default wilcard match to end of list
@q = ((q || 1.0).to_f * 100).to_i
end
@ -44,22 +48,37 @@ module Mime
LOOKUP[string]
end
def register(string, symbol, synonyms = [])
Mime.send :const_set, symbol.to_s.upcase, Type.new(string, symbol, synonyms)
SET << Mime.send(:const_get, symbol.to_s.upcase)
LOOKUP[string] = EXTENSION_LOOKUP[symbol.to_s] = SET.last
def lookup_by_extension(extension)
EXTENSION_LOOKUP[extension]
end
# Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
# rendering different HTML versions depending on the user agent, like an iPhone.
def register_alias(string, symbol, extension_synonyms = [])
register(string, symbol, [], extension_synonyms, true)
end
def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
Mime.instance_eval { const_set symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms) }
SET << Mime.const_get(symbol.to_s.upcase)
([string] + mime_type_synonyms).each { |string| LOOKUP[string] = SET.last } unless skip_lookup
([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last }
end
def parse(accept_header)
# keep track of creation order to keep the subsequent sort stable
index = 0
list = accept_header.split(/,/).map! do |i|
AcceptItem.new(index += 1, *i.split(/;\s*q=/))
end.sort!
list = []
accept_header.split(/,/).each_with_index do |header, index|
params = header.split(/;\s*q=/)
list << AcceptItem.new(index, *params) unless params.empty?
end
list.sort!
# Take care of the broken text/xml entry by renaming or deleting it
text_xml = list.index("text/xml")
app_xml = list.index("application/xml")
app_xml = list.index(Mime::XML.to_s)
if text_xml && app_xml
# set the q value to the max of the two
@ -73,9 +92,9 @@ module Mime
# delete text_xml from the list
list.delete_at(text_xml)
elsif text_xml
list[text_xml].name = "application/xml"
list[text_xml].name = Mime::XML.to_s
end
# Look for more specific xml-based types and sort them ahead of app/xml
@ -128,73 +147,17 @@ module Mime
def ==(mime_type)
(@synonyms + [ self ]).any? { |synonym| synonym.to_s == mime_type.to_s } if mime_type
end
private
def method_missing(method, *args)
if method.to_s =~ /(\w+)\?$/
mime_type = $1.downcase.to_sym
mime_type == @symbol || (mime_type == :html && @symbol == :all)
else
super
end
end
end
ALL = Type.new "*/*", :all
TEXT = Type.new "text/plain", :text
HTML = Type.new "text/html", :html, %w( application/xhtml+xml )
JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript )
ICS = Type.new "text/calendar", :ics
CSV = Type.new "text/csv", :csv
XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml )
RSS = Type.new "application/rss+xml", :rss
ATOM = Type.new "application/atom+xml", :atom
YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml )
JSON = Type.new "application/json", :json, %w( text/x-json )
SET = [ ALL, TEXT, HTML, JS, ICS, XML, RSS, ATOM, YAML, JSON ]
LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k == "" }
LOOKUP["*/*"] = ALL
LOOKUP["text/plain"] = TEXT
LOOKUP["text/html"] = HTML
LOOKUP["application/xhtml+xml"] = HTML
LOOKUP["text/javascript"] = JS
LOOKUP["application/javascript"] = JS
LOOKUP["application/x-javascript"] = JS
LOOKUP["text/calendar"] = ICS
LOOKUP["text/csv"] = CSV
LOOKUP["application/xml"] = XML
LOOKUP["text/xml"] = XML
LOOKUP["application/x-xml"] = XML
LOOKUP["text/yaml"] = YAML
LOOKUP["application/x-yaml"] = YAML
LOOKUP["application/rss+xml"] = RSS
LOOKUP["application/atom+xml"] = ATOM
LOOKUP["application/json"] = JSON
LOOKUP["text/x-json"] = JSON
EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k == "" }
EXTENSION_LOOKUP["html"] = HTML
EXTENSION_LOOKUP["xhtml"] = HTML
EXTENSION_LOOKUP["txt"] = TEXT
EXTENSION_LOOKUP["xml"] = XML
EXTENSION_LOOKUP["js"] = JS
EXTENSION_LOOKUP["ics"] = ICS
EXTENSION_LOOKUP["csv"] = CSV
EXTENSION_LOOKUP["yml"] = YAML
EXTENSION_LOOKUP["yaml"] = YAML
EXTENSION_LOOKUP["rss"] = RSS
EXTENSION_LOOKUP["atom"] = ATOM
EXTENSION_LOOKUP["json"] = JSON
end
require 'action_controller/mime_types'

View file

@ -0,0 +1,20 @@
# Build list of Mime types for HTTP responses
# http://www.iana.org/assignments/media-types/
Mime::Type.register "*/*", :all
Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
# http://www.ietf.org/rfc/rfc4627.txt
Mime::Type.register "application/json", :json, %w( text/x-json )

View file

@ -1,408 +0,0 @@
module ActionController
# === Action Pack pagination for Active Record collections
#
# DEPRECATION WARNING: Pagination will be moved to a plugin in Rails 2.0.
# Install the classic_pagination plugin for forward compatibility:
# script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
#
# The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
# the magic isn't flexible enough for your needs, you can create your own
# paginators with a minimal amount of code.
#
# The Pagination module can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# Pagination is included automatically for all controllers.
#
# For help rendering pagination links, see
# ActionView::Helpers::PaginationHelper.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# model :person
#
# paginate :people, :order => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a <tt>@people</tt>
# instance variable, which is an ordered collection of model objects for the
# current page (at most 20, sorted by last name and first name), and a
# <tt>@person_pages</tt> Paginator instance. The current page is determined
# by the <tt>params[:page]</tt> variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
# and <tt>@people</tt> for a single action, and uses the default of 10 items
# per page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
# @people = Person.find :all, :order => 'last_name, first_name',
# :limit => @person_pages.items_per_page,
# :offset => @person_pages.current.offset
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
#
module Pagination
unless const_defined?(:OPTIONS)
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:singular_name => nil,
:per_page => 10,
:conditions => nil,
:order_by => nil,
:order => nil,
:join => nil,
:joins => nil,
:count => nil,
:include => nil,
:select => nil,
:parameter => 'page'
}
end
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = DEFAULT_OPTIONS.keys
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] ||= Inflector.singularize(collection_id.to_s)
options[:class_name] ||= Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances
# for the paginator's current page. This is designed to be used in a
# single action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name
# <tt>:per_page</tt>:: the maximum number of items to include in a
# single page. Defaults to 10
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
# Model.count
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
#
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
#
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
deprecate :paginate => 'Pagination is moving to a plugin in Rails 2.0: script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination'
# These methods become class methods on any controller
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active
# Record model for all actions in a controller (or certain actions if
# specified with the <tt>:actions</tt> option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# <tt>:actions</tt>:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action)
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
deprecate :paginate => 'Pagination is moving to a plugin in Rails 2.0: script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination'
end
def create_paginators_and_retrieve_collections #:nodoc:
Pagination::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if
options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, options)
model.count(:conditions => options[:conditions],
:joins => options[:join] || options[:joins],
:include => options[:include],
:select => options[:count])
end
# Returns a collection of items for the given +model+ and +options[conditions]+,
# ordered by +options[order]+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, options, paginator)
model.find(:all, :conditions => options[:conditions],
:order => options[:order_by] || options[:order],
:joins => options[:join] || options[:joins], :include => options[:include],
:select => options[:select], :limit => options[:per_page],
:offset => paginator.current.offset)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination,
:find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize
page = params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items
# of size +item_count+ and having +items_per_page+ items per page.
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
# than or equal to zero). The page CGI parameter for links defaults to
# "page" and can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1)
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@pages = {}
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page_number = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
@current_page ||= self[@current_page_number]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
@first_page ||= self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
@last_page ||= self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
@page_count ||= @item_count.zero? ? 1 :
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index
# +number+.
def [](number)
@pages[number] ||= Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index
# +number+. If +number+ is not in the range of valid page numbers or
# is not a number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the
# same page number).
def ==(page)
return false if page.nil?
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this
# page, or nil if this is the first page.
def previous
if first? then nil else @paginator[@number - 1] end
end
# Returns a new Page object representing the page just after this
# page, or nil if this is the last page.
def next
if last? then nil else @paginator[@number + 1] end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns the limit/offset array for this page.
def to_sql
[@paginator.items_per_page, offset]
end
def to_param #:nodoc:
@number.to_s
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
end

View file

@ -0,0 +1,88 @@
module ActionController
module PolymorphicRoutes
def polymorphic_url(record_or_hash_or_array, options = {})
record = extract_record(record_or_hash_or_array)
namespace = extract_namespace(record_or_hash_or_array)
args = case record_or_hash_or_array
when Hash; [ record_or_hash_or_array ]
when Array; record_or_hash_or_array.dup
else [ record_or_hash_or_array ]
end
inflection =
case
when options[:action] == "new"
args.pop
:singular
when record.respond_to?(:new_record?) && record.new_record?
args.pop
:plural
else
:singular
end
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options)
send!(named_route, *args)
end
def polymorphic_path(record_or_hash_or_array)
polymorphic_url(record_or_hash_or_array, :routing_type => :path)
end
%w(edit new formatted).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
def #{action}_polymorphic_url(record_or_hash)
polymorphic_url(record_or_hash, :action => "#{action}")
end
def #{action}_polymorphic_path(record_or_hash)
polymorphic_url(record_or_hash, :action => "#{action}", :routing_type => :path)
end
EOT
end
private
def action_prefix(options)
options[:action] ? "#{options[:action]}_" : ""
end
def routing_type(options)
"#{options[:routing_type] || "url"}"
end
def build_named_route_call(records, namespace, inflection, options = {})
records = Array.new([extract_record(records)]) unless records.is_a?(Array)
base_segment = "#{RecordIdentifier.send!("#{inflection}_class_name", records.pop)}_"
method_root = records.reverse.inject(base_segment) do |string, name|
segment = "#{RecordIdentifier.send!("singular_class_name", name)}_"
segment << string
end
action_prefix(options) + namespace + method_root + routing_type(options)
end
def extract_record(record_or_hash_or_array)
case record_or_hash_or_array
when Array; record_or_hash_or_array.last
when Hash; record_or_hash_or_array[:id]
else record_or_hash_or_array
end
end
def extract_namespace(record_or_hash_or_array)
returning "" do |namespace|
if record_or_hash_or_array.is_a?(Array)
record_or_hash_or_array.delete_if do |record_or_namespace|
if record_or_namespace.is_a?(String) || record_or_namespace.is_a?(Symbol)
namespace << "#{record_or_namespace.to_s}_"
end
end
end
end
end
end
end

View file

@ -0,0 +1,91 @@
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
# the view actions to a higher logical level. Example:
#
# # routes
# map.resources :posts
#
# # view
# <% div_for(post) do %> <div id="post_45" class="post">
# <%= post.body %> What a wonderful world!
# <% end %> </div>
#
# # controller
# def destroy
# post = Post.find(params[:id])
# post.destroy
#
# respond_to do |format|
# format.html { redirect_to(post) } # Calls polymorphic_url(post) which in turn calls post_url(post)
# format.js do
# # Calls: new Effect.fade('post_45');
# render(:update) { |page| page[post].visual_effect(:fade) }
# end
# end
# end
#
# As the example above shows, you can stop caring to a large extent what the actual id of the post is. You just know
# that one is being assigned and that the subsequent calls in redirect_to and the RJS expect that same naming
# convention and allows you to write less code if you follow it.
module RecordIdentifier
extend self
# Returns plural/singular for a record or class. Example:
#
# partial_path(post) # => "posts/post"
# partial_path(Person) # => "people/person"
def partial_path(record_or_class)
klass = class_from_record_or_class(record_or_class)
"#{klass.name.tableize}/#{klass.name.demodulize.underscore}"
end
# The DOM class convention is to use the singular form of an object or class. Examples:
#
# dom_class(post) # => "post"
# dom_class(Person) # => "person"
#
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
#
# dom_class(post, :edit) # => "edit_post"
# dom_class(Person, :edit) # => "edit_person"
def dom_class(record_or_class, prefix = nil)
[ prefix, singular_class_name(record_or_class) ].compact * '_'
end
# The DOM class convention is to use the singular form of an object or class with the id following an underscore.
# If no id is found, prefix with "new_" instead. Examples:
#
# dom_class(Post.new(:id => 45)) # => "post_45"
# dom_class(Post.new) # => "new_post"
#
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
#
# dom_class(Post.new(:id => 45), :edit) # => "edit_post_45"
def dom_id(record, prefix = nil)
prefix ||= 'new' unless record.id
[ prefix, singular_class_name(record), record.id ].compact * '_'
end
# Returns the plural class name of a record or class. Examples:
#
# plural_class_name(post) # => "posts"
# plural_class_name(Highrise::Person) # => "highrise_people"
def plural_class_name(record_or_class)
singular_class_name(record_or_class).pluralize
end
# Returns the singular class name of a record or class. Examples:
#
# singular_class_name(post) # => "post"
# singular_class_name(Highrise::Person) # => "highrise_person"
def singular_class_name(record_or_class)
class_from_record_or_class(record_or_class).name.underscore.tr('/', '_')
end
private
def class_from_record_or_class(record_or_class)
record_or_class.is_a?(Class) ? record_or_class : record_or_class.class
end
end
end

View file

@ -1,27 +1,38 @@
require 'tempfile'
require 'stringio'
require 'strscan'
module ActionController
# Subclassing AbstractRequest makes these methods available to the request objects used in production and testing,
# CgiRequest and TestRequest
# HTTP methods which are accepted by default.
ACCEPTED_HTTP_METHODS = Set.new(%w( get head put post delete options ))
# CgiRequest and TestRequest provide concrete implementations.
class AbstractRequest
cattr_accessor :relative_url_root
remove_method :relative_url_root
# Returns the hash of environment variables for this request,
# The hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }.
attr_reader :env
# Returns both GET and POST parameters in a single hash.
def parameters
@parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access
# The true HTTP request method as a lowercase symbol, such as :get.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
def request_method
@request_method ||= begin
method = ((@env['REQUEST_METHOD'] == 'POST' && !parameters[:_method].blank?) ? parameters[:_method].to_s : @env['REQUEST_METHOD']).downcase
if ACCEPTED_HTTP_METHODS.include?(method)
method.to_sym
else
raise UnknownHttpMethod, "#{method}, accepted HTTP methods are #{ACCEPTED_HTTP_METHODS.to_a.to_sentence}"
end
end
end
# Returns the HTTP request method as a lowercase symbol (:get, for example). Note, HEAD is returned as :get
# since the two are supposedly to be functionaly equivilent for all purposes except that HEAD won't return a response
# body (which Rails also takes care of elsewhere).
# The HTTP request method as a lowercase symbol, such as :get.
# Note, HEAD is returned as :get since the two are functionally
# equivalent from the application's perspective.
def method
@request_method ||= (!parameters[:_method].blank? && @env['REQUEST_METHOD'] == 'POST') ?
parameters[:_method].to_s.downcase.to_sym :
@env['REQUEST_METHOD'].downcase.to_sym
@request_method == :head ? :get : @request_method
request_method == :head ? :get : request_method
end
# Is this a GET (or HEAD) request? Equivalent to request.method == :get
@ -31,63 +42,83 @@ module ActionController
# Is this a POST request? Equivalent to request.method == :post
def post?
method == :post
request_method == :post
end
# Is this a PUT request? Equivalent to request.method == :put
def put?
method == :put
request_method == :put
end
# Is this a DELETE request? Equivalent to request.method == :delete
def delete?
method == :delete
request_method == :delete
end
# Is this a HEAD request? HEAD is mapped as :get for request.method, so here we ask the
# REQUEST_METHOD header directly. Thus, for head, both get? and head? will return true.
# Is this a HEAD request? request.method sees HEAD as :get, so check the
# HTTP method directly.
def head?
@env['REQUEST_METHOD'].downcase.to_sym == :head
request_method == :head
end
# Determine whether the body of a HTTP call is URL-encoded (default)
# or matches one of the registered param_parsers.
def headers
@env
end
def content_length
@content_length ||= env['CONTENT_LENGTH'].to_i
end
# The MIME type of the HTTP request, such as Mime::XML.
#
# For backward compatibility, the post format is extracted from the
# X-Post-Data-Format HTTP header if present.
def content_type
@content_type ||=
begin
content_type = @env['CONTENT_TYPE'].to_s.downcase
if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
case x_post_format.to_s.downcase
when 'yaml'
content_type = 'application/x-yaml'
when 'xml'
content_type = 'application/xml'
end
end
Mime::Type.lookup(content_type)
end
@content_type ||= Mime::Type.lookup(content_type_without_parameters)
end
# Returns the accepted MIME type for the request
def accepts
@accepts ||=
if @env['HTTP_ACCEPT'].to_s.strip.empty?
[ content_type, Mime::ALL ]
[ content_type, Mime::ALL ].compact # make sure content_type being nil is not included
else
Mime::Type.parse(@env['HTTP_ACCEPT'])
end
end
# Returns the Mime type for the format used in the request. If there is no format available, the first of the
# accept types will be used. Examples:
#
# GET /posts/5.xml | request.format => Mime::XML
# GET /posts/5.xhtml | request.format => Mime::HTML
# GET /posts/5 | request.format => request.accepts.first (usually Mime::HTML for browsers)
def format
@format ||= parameters[:format] ? Mime::Type.lookup_by_extension(parameters[:format]) : accepts.first
end
# Sets the format by string extension, which can be used to force custom formats that are not controlled by the extension.
# Example:
#
# class ApplicationController < ActionController::Base
# before_filter :adjust_format_for_iphone
#
# private
# def adjust_format_for_iphone
# request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
# end
# end
def format=(extension)
parameters[:format] = extension.to_s
format
end
# Returns true if the request's "X-Requested-With" header contains
# "XMLHttpRequest". (The Prototype Javascript library sends this header with
# every Ajax request.)
def xml_http_request?
not /XMLHttpRequest/i.match(@env['HTTP_X_REQUESTED_WITH']).nil?
!(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
end
alias xhr? :xml_http_request?
@ -97,12 +128,17 @@ module ActionController
# falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
# delimited list in the case of multiple chained proxies; the first is
# the originating IP.
#
# Security note: do not use if IP spoofing is a concern for your
# application. Since remote_ip checks HTTP headers for addresses forwarded
# by proxies, the client may send any IP. remote_addr can't be spoofed but
# also doesn't work behind a proxy, since it's always the proxy's IP.
def remote_ip
return @env['HTTP_CLIENT_IP'] if @env.include? 'HTTP_CLIENT_IP'
if @env.include? 'HTTP_X_FORWARDED_FOR' then
remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',').reject do |ip|
ip =~ /^unknown$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
ip.strip =~ /^unknown$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
end
return remote_ips.first.strip unless remote_ips.empty?
@ -111,46 +147,15 @@ module ActionController
@env['REMOTE_ADDR']
end
# Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = 1)
return nil if !/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.match(host).nil? or host.nil?
host.split('.').last(1 + tld_length).join('.')
# Returns the lowercase name of the HTTP server software.
def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end
# Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org".
# You can specify a different <tt>tld_length</tt>, such as 2 to catch ["www"] instead of ["www", "rubyonrails"]
# in "www.rubyonrails.co.uk".
def subdomains(tld_length = 1)
return [] unless host
parts = host.split('.')
parts[0..-(tld_length+2)]
end
# Receive the raw post data.
# This is useful for services such as REST, XMLRPC and SOAP
# which communicate over HTTP POST but don't use the traditional parameter format.
def raw_post
@env['RAW_POST_DATA']
end
# Return the request URI, accounting for server idiosyncracies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri
if uri = @env['REQUEST_URI']
# Remove domain, which webrick puts into the request_uri.
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
else
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
uri << '?' << env_qs
end
@env['REQUEST_URI'] = uri
end
# Returns the complete URL used for this request
def url
protocol + host_with_port + request_uri
end
# Return 'https://' if this is an SSL request and 'http://' otherwise.
@ -163,28 +168,14 @@ module ActionController
@env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end
# Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
def path
path = (uri = request_uri) ? uri.split('?').first : ''
# Cut off the path to the installation directory if given
path.sub!(%r/^#{relative_url_root}/, '')
path || ''
# Returns the host for this request, such as example.com.
def host
end
# Returns the path minus the web server relative installation directory.
# This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
# It can be automatically extracted for Apache setups. If the server is not
# Apache, this method returns an empty string.
def relative_url_root
@@relative_url_root ||= case
when @env["RAILS_RELATIVE_URL_ROOT"]
@env["RAILS_RELATIVE_URL_ROOT"]
when server_software == 'apache'
@env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
else
''
end
# Returns a host:port string for this request, such as example.com or
# example.com:8080.
def host_with_port
@host_with_port ||= host + port_string
end
# Returns the port number of this request as an integer.
@ -206,10 +197,94 @@ module ActionController
(port == standard_port) ? '' : ":#{port}"
end
# Returns a host:port string for this request, such as example.com or
# example.com:8080.
def host_with_port
host + port_string
# Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = 1)
return nil unless named_host?(host)
host.split('.').last(1 + tld_length).join('.')
end
# Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org".
# You can specify a different <tt>tld_length</tt>, such as 2 to catch ["www"] instead of ["www", "rubyonrails"]
# in "www.rubyonrails.co.uk".
def subdomains(tld_length = 1)
return [] unless named_host?(host)
parts = host.split('.')
parts[0..-(tld_length+2)]
end
# Return the query string, accounting for server idiosyncracies.
def query_string
if uri = @env['REQUEST_URI']
uri.split('?', 2)[1] || ''
else
@env['QUERY_STRING'] || ''
end
end
# Return the request URI, accounting for server idiosyncracies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
def request_uri
if uri = @env['REQUEST_URI']
# Remove domain, which webrick puts into the request_uri.
(%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
else
# Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
uri = @env['PATH_INFO']
uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil?
unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty?
uri << '?' << env_qs
end
if uri.nil?
@env.delete('REQUEST_URI')
uri
else
@env['REQUEST_URI'] = uri
end
end
end
# Returns the interpreted path to requested resource after all the installation directory of this application was taken into account
def path
path = (uri = request_uri) ? uri.split('?').first.to_s : ''
# Cut off the path to the installation directory if given
path.sub!(%r/^#{relative_url_root}/, '')
path || ''
end
# Returns the path minus the web server relative installation directory.
# This can be set with the environment variable RAILS_RELATIVE_URL_ROOT.
# It can be automatically extracted for Apache setups. If the server is not
# Apache, this method returns an empty string.
def relative_url_root
@@relative_url_root ||= case
when @env["RAILS_RELATIVE_URL_ROOT"]
@env["RAILS_RELATIVE_URL_ROOT"]
when server_software == 'apache'
@env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '')
else
''
end
end
# Read the request body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
unless env.include? 'RAW_POST_DATA'
env['RAW_POST_DATA'] = body.read(content_length)
body.rewind if body.respond_to?(:rewind)
end
env['RAW_POST_DATA']
end
# Returns both GET and POST parameters in a single hash.
def parameters
@parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
end
def path_parameters=(parameters) #:nodoc:
@ -222,33 +297,31 @@ module ActionController
@symbolized_path_parameters ||= path_parameters.symbolize_keys
end
# Returns a hash with the parameters used to form the path of the request
# Returns a hash with the parameters used to form the path of the request.
# Returned hash keys are strings. See <tt>symbolized_path_parameters</tt> for symbolized keys.
#
# Example:
#
# {:action => 'my_action', :controller => 'my_controller'}
# {'action' => 'my_action', 'controller' => 'my_controller'}
def path_parameters
@path_parameters ||= {}
end
# Returns the lowercase name of the HTTP server software.
def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end
#--
# Must be implemented in the concrete request
#++
# The request body is an IO input stream.
def body
end
def query_parameters #:nodoc:
end
def request_parameters #:nodoc:
end
# Returns the host for this request, such as example.com.
def host
end
def cookies #:nodoc:
end
@ -261,5 +334,397 @@ module ActionController
def reset_session #:nodoc:
end
protected
# The raw content type string. Use when you need parameters such as
# charset or boundary which aren't included in the content_type MIME type.
# Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
def content_type_with_parameters
content_type_from_legacy_post_data_format_header ||
env['CONTENT_TYPE'].to_s
end
# The raw content type string with its parameters stripped off.
def content_type_without_parameters
@content_type_without_parameters ||= self.class.extract_content_type_without_parameters(content_type_with_parameters)
end
private
def content_type_from_legacy_post_data_format_header
if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
case x_post_format.to_s.downcase
when 'yaml'; 'application/x-yaml'
when 'xml'; 'application/xml'
end
end
end
def parse_formatted_request_parameters
return {} if content_length.zero?
content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
# Don't parse params for unknown requests.
return {} if content_type.blank?
mime_type = Mime::Type.lookup(content_type)
strategy = ActionController::Base.param_parsers[mime_type]
# Only multipart form parsing expects a stream.
body = (strategy && strategy != :multipart_form) ? raw_post : self.body
case strategy
when Proc
strategy.call(body)
when :url_encoded_form
self.class.clean_up_ajax_request_body! body
self.class.parse_query_parameters(body)
when :multipart_form
self.class.parse_multipart_form_parameters(body, boundary, content_length, env)
when :xml_simple, :xml_node
body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
when :yaml
YAML.load(body)
else
{}
end
rescue Exception => e # YAML, XML or Ruby code block errors
raise
{ "body" => body,
"content_type" => content_type_with_parameters,
"content_length" => content_length,
"exception" => "#{e.message} (#{e.class})",
"backtrace" => e.backtrace }
end
def named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end
class << self
def parse_query_parameters(query_string)
return {} if query_string.blank?
pairs = query_string.split('&').collect do |chunk|
next if chunk.empty?
key, value = chunk.split('=', 2)
next if key.empty?
value = value.nil? ? nil : CGI.unescape(value)
[ CGI.unescape(key), value ]
end.compact
UrlEncodedPairParser.new(pairs).result
end
def parse_request_parameters(params)
parser = UrlEncodedPairParser.new
params = params.dup
until params.empty?
for key, value in params
if key.blank?
params.delete key
elsif !key.include?('[')
# much faster to test for the most common case first (GET)
# and avoid the call to build_deep_hash
parser.result[key] = get_typed_value(value[0])
params.delete key
elsif value.is_a?(Array)
parser.parse(key, get_typed_value(value.shift))
params.delete key if value.empty?
else
raise TypeError, "Expected array, found #{value.inspect}"
end
end
end
parser.result
end
def parse_multipart_form_parameters(body, boundary, content_length, env)
parse_request_parameters(read_multipart(body, boundary, content_length, env))
end
def extract_multipart_boundary(content_type_with_parameters)
if content_type_with_parameters =~ MULTIPART_BOUNDARY
['multipart/form-data', $1.dup]
else
extract_content_type_without_parameters(content_type_with_parameters)
end
end
def extract_content_type_without_parameters(content_type_with_parameters)
$1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
end
def clean_up_ajax_request_body!(body)
body.chop! if body[-1] == 0
body.gsub!(/&_=$/, '')
end
private
def get_typed_value(value)
case value
when String
value
when NilClass
''
when Array
value.map { |v| get_typed_value(v) }
else
if value.is_a?(UploadedFile)
# Uploaded file
if value.original_filename
value
# Multipart param
else
result = value.read
value.rewind
result
end
# Unknown value, neither string nor multipart.
else
raise "Unknown form value: #{value.inspect}"
end
end
end
MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
EOL = "\015\012"
def read_multipart(body, boundary, content_length, env)
params = Hash.new([])
boundary = "--" + boundary
quoted_boundary = Regexp.quote(boundary, "n")
buf = ""
bufsize = 10 * 1024
boundary_end=""
# start multipart/form-data
body.binmode if defined? body.binmode
boundary_size = boundary.size + EOL.size
content_length -= boundary_size
status = body.read(boundary_size)
if nil == status
raise EOFError, "no content body"
elsif boundary + EOL != status
raise EOFError, "bad content body"
end
loop do
head = nil
content =
if 10240 < content_length
UploadedTempfile.new("CGI")
else
UploadedStringIO.new
end
content.binmode if defined? content.binmode
until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
if (not head) and /#{EOL}#{EOL}/n.match(buf)
buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
head = $1.dup
""
end
next
end
if head and ( (EOL + boundary + EOL).size < buf.size )
content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
end
c = if bufsize < content_length
body.read(bufsize)
else
body.read(content_length)
end
if c.nil? || c.empty?
raise EOFError, "bad content body"
end
buf.concat(c)
content_length -= c.size
end
buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
content.print $1
if "--" == $2
content_length = -1
end
boundary_end = $2.dup
""
end
content.rewind
head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
if filename = $1 || $2
if /Mac/ni.match(env['HTTP_USER_AGENT']) and
/Mozilla/ni.match(env['HTTP_USER_AGENT']) and
(not /MSIE/ni.match(env['HTTP_USER_AGENT']))
filename = CGI.unescape(filename)
end
content.original_path = filename.dup
end
head =~ /Content-Type: ([^\r]*)/ni
content.content_type = $1.dup if $1
head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
name = $1.dup if $1
if params.has_key?(name)
params[name].push(content)
else
params[name] = [content]
end
break if buf.size == 0
break if content_length == -1
end
raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
begin
body.rewind if body.respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
params
end
end
end
class UrlEncodedPairParser < StringScanner #:nodoc:
attr_reader :top, :parent, :result
def initialize(pairs = [])
super('')
@result = {}
pairs.each { |key, value| parse(key, value) }
end
KEY_REGEXP = %r{([^\[\]=&]+)}
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
# Parse the query string
def parse(key, value)
self.string = key
@top, @parent = result, nil
# First scan the bare key
key = scan(KEY_REGEXP) or return
key = post_key_check(key)
# Then scan as many nestings as present
until eos?
r = scan(BRACKETED_KEY_REGEXP) or return
key = self[1]
key = post_key_check(key)
end
bind(key, value)
end
private
# After we see a key, we must look ahead to determine our next action. Cases:
#
# [] follows the key. Then the value must be an array.
# = follows the key. (A value comes next)
# & or the end of string follows the key. Then the key is a flag.
# otherwise, a hash follows the key.
def post_key_check(key)
if scan(/\[\]/) # a[b][] indicates that b is an array
container(key, Array)
nil
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
container(key, Hash)
nil
else # End of key? We do nothing.
key
end
end
# Add a container to the stack.
def container(key, klass)
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
value = bind(key, klass.new)
type_conflict! klass, value unless value.is_a?(klass)
push(value)
end
# Push a value onto the 'stack', which is actually only the top 2 items.
def push(value)
@parent, @top = @top, value
end
# Bind a key (which may be nil for items in an array) to the provided value.
def bind(key, value)
if top.is_a? Array
if key
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
top[-1][key] = value
else
top << {key => value}.with_indifferent_access
push top.last
end
else
top << value
end
elsif top.is_a? Hash
key = CGI.unescape(key)
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
return top[key] ||= value
else
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
end
return value
end
def type_conflict!(klass, value)
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value."
end
end
module UploadedFile
def self.included(base)
base.class_eval do
attr_accessor :original_path, :content_type
alias_method :local_path, :path
end
end
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
unless defined? @original_filename
@original_filename =
unless original_path.blank?
if original_path =~ /^(?:.*[:\\\/])?(.*)/m
$1
else
File.basename original_path
end
end
end
@original_filename
end
end
class UploadedStringIO < StringIO
include UploadedFile
end
class UploadedTempfile < Tempfile
include UploadedFile
end
end

View file

@ -0,0 +1,132 @@
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
module RequestForgeryProtection
def self.included(base)
base.class_eval do
class_inheritable_accessor :request_forgery_protection_options
self.request_forgery_protection_options = {}
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
base.extend(ClassMethods)
end
module ClassMethods
# Protect a controller's actions from CSRF attacks by ensuring that all forms are coming from the current web application, not
# a forged link from another site. This is done by embedding a token based on the session (which an attacker wouldn't know) in
# all forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only
# HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication
# scheme there anyway). Also, GET requests are not protected as these should be indempotent anyway.
#
# You turn this on with the #protect_from_forgery method, which will perform the check and raise
# an ActionController::InvalidAuthenticityToken if the token doesn't match what was expected. And it will add
# a _authenticity_token parameter to all forms that are automatically generated by Rails. You can customize the error message
# given through public/422.html.
#
# Learn more about CSRF (Cross-Site Request Forgery) attacks:
#
# * http://isc.sans.org/diary.html?storyid=1750
# * http://en.wikipedia.org/wiki/Cross-site_request_forgery
#
# Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application.
# There are a few guidelines you should follow:
#
# * Keep your GET requests safe and idempotent. More reading material:
# * http://www.xml.com/pub/a/2002/04/24/deviant.html
# * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
# * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look for "Expires: at end of session"
#
# If you need to construct a request yourself, but still want to take advantage of forgery protection, you can grab the
# authenticity_token using the form_authenticity_token helper method and make it part of the parameters yourself.
#
# Example:
#
# class FooController < ApplicationController
# # uses the cookie session store (then you don't need a separate :secret)
# protect_from_forgery :except => :index
#
# # uses one of the other session stores that uses a session_id value.
# protect_from_forgery :secret => 'my-little-pony', :except => :index
#
# # you can disable csrf protection on controller-by-controller basis:
# skip_before_filter :verify_authenticity_token
# end
#
# If you are upgrading from Rails 1.x, disable forgery protection to
# simplify your tests. Add this to config/environments/test.rb:
#
# # Disable request forgery protection in test environment
# config.action_controller.allow_forgery_protection = false
#
# Valid Options:
#
# * <tt>:only/:except</tt> - passed to the before_filter call. Set which actions are verified.
# * <tt>:secret</tt> - Custom salt used to generate the form_authenticity_token.
# Leave this off if you are using the cookie session store.
# * <tt>:digest</tt> - Message digest used for hashing. Defaults to 'SHA1'
def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token
before_filter :verify_authenticity_token, :only => options.delete(:only), :except => options.delete(:except)
request_forgery_protection_options.update(options)
end
end
protected
# The actual before_filter that is used. Modify this to change how you handle unverified requests.
def verify_authenticity_token
verified_request? || raise(ActionController::InvalidAuthenticityToken)
end
# Returns true or false if a request is verified. Checks:
#
# * is the format restricted? By default, only HTML and AJAX requests are checked.
# * is it a GET request? Gets should be safe and idempotent
# * Does the form_authenticity_token match the given _token value from the params?
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
end
def verifiable_request_format?
request.format.html? || request.format.js?
end
# Sets the token value for the current session. Pass a :secret option in #protect_from_forgery to add a custom salt to the hash.
def form_authenticity_token
@form_authenticity_token ||= if request_forgery_protection_options[:secret]
authenticity_token_from_session_id
elsif session.respond_to?(:dbman) && session.dbman.respond_to?(:generate_digest)
authenticity_token_from_cookie_session
elsif session.nil?
raise InvalidAuthenticityToken, "Request Forgery Protection requires a valid session. Use #allow_forgery_protection to disable it, or use a valid session."
else
raise InvalidAuthenticityToken, "No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store)."
end
end
# Generates a unique digest using the session_id and the CSRF secret.
def authenticity_token_from_session_id
key = if request_forgery_protection_options[:secret].respond_to?(:call)
request_forgery_protection_options[:secret].call(@session)
else
request_forgery_protection_options[:secret]
end
digest = request_forgery_protection_options[:digest] ||= 'SHA1'
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(digest), key.to_s, session.session_id.to_s)
end
# No secret was given, so assume this is a cookie session store.
def authenticity_token_from_cookie_session
session[:csrf_id] ||= CGI::Session.generate_unique_id
session.dbman.generate_digest(session[:csrf_id])
end
def protect_against_forgery?
allow_forgery_protection && request_forgery_protection_token
end
end
end

View file

@ -0,0 +1,138 @@
require 'optparse'
require 'action_controller/integration'
module ActionController
class RequestProfiler
# Wrap up the integration session runner.
class Sandbox
include Integration::Runner
def self.benchmark(n, script)
new(script).benchmark(n)
end
def initialize(script_path)
@quiet = false
define_run_method(File.read(script_path))
reset!
end
def benchmark(n)
@quiet = true
print ' '
result = Benchmark.realtime do
n.times do |i|
run
print i % 10 == 0 ? 'x' : '.'
$stdout.flush
end
end
puts
result
ensure
@quiet = false
end
def say(message)
puts " #{message}" unless @quiet
end
private
def define_run_method(script)
instance_eval "def run; #{script}; end", __FILE__, __LINE__
end
end
attr_reader :options
def initialize(options = {})
@options = default_options.merge(options)
end
def self.run(args = nil, options = {})
profiler = new(options)
profiler.parse_options(args) if args
profiler.run
end
def run
sandbox = Sandbox.new(options[:script])
puts 'Warming up once'
elapsed = warmup(sandbox)
puts '%.2f sec, %d requests, %d req/sec' % [elapsed, sandbox.request_count, sandbox.request_count / elapsed]
puts "\n#{options[:benchmark] ? 'Benchmarking' : 'Profiling'} #{options[:n]}x"
options[:benchmark] ? benchmark(sandbox) : profile(sandbox)
end
def profile(sandbox)
load_ruby_prof
results = RubyProf.profile { benchmark(sandbox) }
show_profile_results results
results
end
def benchmark(sandbox)
sandbox.request_count = 0
elapsed = sandbox.benchmark(options[:n]).to_f
count = sandbox.request_count.to_i
puts '%.2f sec, %d requests, %d req/sec' % [elapsed, count, count / elapsed]
end
def warmup(sandbox)
Benchmark.realtime { sandbox.run }
end
def default_options
{ :n => 100, :open => 'open %s &' }
end
# Parse command-line options
def parse_options(args)
OptionParser.new do |opt|
opt.banner = "USAGE: #{$0} [options] [session script path]"
opt.on('-n', '--times [0000]', 'How many requests to process. Defaults to 100.') { |v| options[:n] = v.to_i }
opt.on('-b', '--benchmark', 'Benchmark instead of profiling') { |v| options[:benchmark] = v }
opt.on('--open [CMD]', 'Command to open profile results. Defaults to "open %s &"') { |v| options[:open] = v }
opt.on('-h', '--help', 'Show this help') { puts opt; exit }
opt.parse args
if args.empty?
puts opt
exit
end
options[:script] = args.pop
end
end
protected
def load_ruby_prof
begin
require 'ruby-prof'
#RubyProf.measure_mode = RubyProf::ALLOCATED_OBJECTS
rescue LoadError
abort '`gem install ruby-prof` to use the profiler'
end
end
def show_profile_results(results)
File.open "#{RAILS_ROOT}/tmp/profile-graph.html", 'w' do |file|
RubyProf::GraphHtmlPrinter.new(results).print(file)
`#{options[:open] % file.path}` if options[:open]
end
File.open "#{RAILS_ROOT}/tmp/profile-flat.txt", 'w' do |file|
RubyProf::FlatPrinter.new(results).print(file)
`#{options[:open] % file.path}` if options[:open]
end
end
end
end

View file

@ -1,22 +1,110 @@
module ActionController #:nodoc:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
# 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. So too
# could the decision on whether something is a public or a developer request.
# 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 tailor the rescuing behavior and appearance by overwriting the following two stub methods.
# 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 = {
'ActionController::MissingTemplate' => 'missing_template',
'ActionController::RoutingError' => 'routing_error',
'ActionController::UnknownAction' => 'unknown_action',
'ActionView::TemplateError' => 'template_error'
}
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.class_inheritable_array :rescue_handlers
base.rescue_handlers = []
base.extend(ClassMethods)
base.class_eval do
alias_method_chain :perform_action, :rescue
end
end
module ClassMethods #:nodoc:
def process_with_exception(request, response, exception)
module ClassMethods
def process_with_exception(request, response, exception) #:nodoc:
new.process(request, response, :rescue_action, exception)
end
# Rescue exceptions raised in controller actions.
#
# <tt>rescue_from</tt> receives a series of exception classes or class
# names, and a trailing :with option with the name of a method or a Proc
# object to be called to handle them. Alternatively a block can be given.
#
# Handlers that take one argument will be called with the exception, so
# that the exception can be inspected when dealing with it.
#
# Handlers are inherited. They are searched from right to left, from
# bottom to top, and up the hierarchy. The handler of the first class for
# which exception.is_a?(klass) holds true is the one invoked, if any.
#
# class ApplicationController < ActionController::Base
# rescue_from User::NotAuthorized, :with => :deny_access # self defined exception
# rescue_from ActiveRecord::RecordInvalid, :with => :show_errors
#
# rescue_from 'MyAppError::Base' do |exception|
# render :xml => exception, :status => 500
# end
#
# protected
# def deny_access
# ...
# end
#
# def show_errors(exception)
# exception.record.new_record? ? ...
# end
# end
def rescue_from(*klasses, &block)
options = klasses.extract_options!
unless options.has_key?(:with)
block_given? ? options[:with] = block : raise(ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument.")
end
klasses.each do |klass|
key = if klass.is_a?(Class) && klass <= Exception
klass.name
elsif klass.is_a?(String)
klass
else
raise(ArgumentError, "#{klass} is neither an Exception nor a String")
end
# Order is important, we put the pair at the end. When dealing with an
# exception we will follow the documented order going from right to left.
rescue_handlers << [key, options[:with]]
end
end
end
protected
@ -25,6 +113,12 @@ module ActionController #:nodoc:
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
@ -47,96 +141,118 @@ module ActionController #:nodoc:
end
end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
# 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.s
def rescue_action_in_public(exception) #:doc:
case exception
when RoutingError, UnknownAction
render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found")
else
render_text(IO.read(File.join(RAILS_ROOT, 'public', '500.html')), "500 Internal Error")
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. For example, if a 500 error is
# being handled Rails will first attempt to render the file at <tt>public/500.html</tt>.
# If the file doesn't exist, the body of the response will be left empty.
def render_optional_error_file(status_code)
status = interpret_status(status_code)
path = "#{RAILS_ROOT}/public/#{status[0,3]}.html"
if File.exist?(path)
render :file => path, :status => status
else
head status
end
end
# Overwrite to expand the meaning of a local request in order to show local rescues on other occurrences than
# the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
# remotely.
# 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, request.remote_ip] == ["127.0.0.1"] * 2
request.remote_addr == LOCALHOST and request.remote_ip == LOCALHOST
end
# Renders a detailed diagnostics screen on action exceptions.
# Render detailed diagnostics for unhandled exceptions rescued from
# a controller action.
def rescue_action_locally(exception)
add_variables_to_assigns
@template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.send(:assign_variables_from_controller)
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.send!(:assign_variables_from_controller)
@template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false))
response.content_type = Mime::HTML
render_file(rescues_path("layout"), response_code_for_rescue(exception))
render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
end
# Tries to rescue the exception by looking up and calling a registered handler.
def rescue_action_with_handler(exception)
if handler = handler_for_rescue(exception)
if handler.arity != 0
handler.call(exception)
else
handler.call
end
true # don't rely on the return value of the handler
end
end
private
def perform_action_with_rescue #:nodoc:
begin
perform_action_without_rescue
rescue Exception => exception # errors from action performed
if defined?(Breakpoint) && params["BP-RETRY"]
msg = exception.backtrace.first
if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then
origin_file, origin_line = md[1], md[2].to_i
set_trace_func(lambda do |type, file, line, method, context, klass|
if file == origin_file and line == origin_line then
set_trace_func(nil)
params["BP-RETRY"] = false
callstack = caller
callstack.slice!(0) if callstack.first["rescue.rb"]
file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures
message = "Exception at #{file}:#{line}#{" in `#{method}'" if method}." # `´ ( for ruby-mode)
Breakpoint.handle_breakpoint(context, message, file, line)
end
end)
retry
end
end
rescue_action(exception)
end
perform_action_without_rescue
rescue Exception => exception # errors from action performed
return if rescue_action_with_handler(exception)
rescue_action(exception)
end
def rescues_path(template_name)
File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml"
"#{File.dirname(__FILE__)}/templates/rescues/#{template_name}.erb"
end
def template_path_for_local_rescue(exception)
rescues_path(
case exception
when MissingTemplate then "missing_template"
when RoutingError then "routing_error"
when UnknownAction then "unknown_action"
when ActionView::TemplateError then "template_error"
else "diagnostics"
end
)
rescues_path(rescue_templates[exception.class.name])
end
def response_code_for_rescue(exception)
case exception
when UnknownAction, RoutingError
"404 Page Not Found"
else
"500 Internal Error"
rescue_responses[exception.class.name]
end
def handler_for_rescue(exception)
# We go from right to left because pairs are pushed onto rescue_handlers
# as rescue_from declarations are found.
_, handler = *rescue_handlers.reverse.detect do |klass_name, handler|
# The purpose of allowing strings in rescue_from is to support the
# declaration of handler associations for exception classes whose
# definition is yet unknown.
#
# Since this loop needs the constants it would be inconsistent to
# assume they should exist at this point. An early raised exception
# could trigger some other handler and the array could include
# precisely a string whose corresponding constant has not yet been
# seen. This is why we are tolerant to unknown constants.
#
# Note that this tolerance only matters if the exception was given as
# a string, otherwise a NameError will be raised by the interpreter
# itself when rescue_from CONSTANT is executed.
klass = self.class.const_get(klass_name) rescue nil
klass ||= klass_name.constantize rescue nil
exception.is_a?(klass) if klass
end
case handler
when Symbol
method(handler)
when Proc
handler.bind(self)
end
end
def clean_backtrace(exception)
exception.backtrace.collect { |line| Object.const_defined?(:RAILS_ROOT) ? line.gsub(RAILS_ROOT, "") : line }
if backtrace = exception.backtrace
if defined?(RAILS_ROOT)
backtrace.map { |line| line.sub RAILS_ROOT, '' }
else
backtrace
end
end
end
end
end

View file

@ -1,14 +1,56 @@
module ActionController
# == Overview
#
# ActionController::Resources are a way of defining RESTful resources. A RESTful resource, in basic terms,
# is something that can be pointed at and it will respond with a representation of the data requested.
# In real terms this could mean a user with a browser requests an HTML page, or that a desktop application
# requests XML data.
#
# RESTful design is based on the assumption that there are four generic verbs that a user of an
# application can request from a resource (the noun).
#
# Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used
# denotes the type of action that should take place.
#
# === The Different Methods and their Usage
#
# +GET+ Requests for a resource, no saving or editing of a resource should occur in a GET request
# +POST+ Creation of resources
# +PUT+ Editing of attributes on a resource
# +DELETE+ Deletion of a resource
#
# === Examples
#
# # A GET request on the Posts resource is asking for all Posts
# GET /posts
#
# # A GET request on a single Post resource is asking for that particular Post
# GET /posts/1
#
# # A POST request on the Posts resource is asking for a Post to be created with the supplied details
# POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } }
#
# # A PUT request on a single Post resource is asking for a Post to be updated
# PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } }
#
# # A DELETE request on a single Post resource is asking for it to be deleted
# DELETE /posts # with => { :id => 1 }
#
# By using the REST convention, users of our application can assume certain things about how the data
# is requested and how it is returned. Rails simplifies the routing part of RESTful design by
# supplying you with methods to create them in your routes.rb file.
#
# Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer
module Resources
class Resource #:nodoc:
attr_reader :collection_methods, :member_methods, :new_methods
attr_reader :path_prefix, :new_name_prefix
attr_reader :path_prefix, :name_prefix
attr_reader :plural, :singular
attr_reader :options
def initialize(entities, options)
@plural = entities
@singular = options[:singular] || plural.to_s.singularize
@plural ||= entities
@singular ||= options[:singular] || plural.to_s.singularize
@options = options
@ -18,7 +60,18 @@ module ActionController
end
def controller
@controller ||= (options[:controller] || plural).to_s
@controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}"
end
def requirements(with_id = false)
@requirements ||= @options[:requirements] || {}
@id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ }
with_id ? @requirements.merge(@id_requirement) : @requirements
end
def conditions
@conditions = @options[:conditions] || {}
end
def path
@ -37,26 +90,18 @@ module ActionController
@nesting_path_prefix ||= "#{path}/:#{singular}_id"
end
def deprecate_name_prefix?
@name_prefix.blank? && !@new_name_prefix.blank?
end
def name_prefix
deprecate_name_prefix? ? @new_name_prefix : @name_prefix
end
def old_name_prefix
@name_prefix
end
def nesting_name_prefix
"#{new_name_prefix}#{singular}_"
"#{name_prefix}#{singular}_"
end
def action_separator
@action_separator ||= Base.resource_action_separator
end
def uncountable?
@singular.to_s == @plural.to_s
end
protected
def arrange_actions
@collection_methods = arrange_actions_by_methods(options.delete(:collection))
@ -72,7 +117,6 @@ module ActionController
def set_prefixes
@path_prefix = options.delete(:path_prefix)
@name_prefix = options.delete(:name_prefix)
@new_name_prefix = options.delete(:new_name_prefix)
end
def arrange_actions_by_methods(actions)
@ -89,103 +133,111 @@ module ActionController
class SingletonResource < Resource #:nodoc:
def initialize(entity, options)
@plural = @singular = entity
@options = options
arrange_actions
add_default_actions
set_prefixes
@singular = @plural = entity
options[:controller] ||= @singular.to_s.pluralize
super
end
alias_method :member_path, :path
alias_method :nesting_path_prefix, :path
end
# Creates named routes for implementing verb-oriented controllers. This is
# useful for implementing REST API's, where a single resource has different
# behavior based on the HTTP verb (method) used to access it.
#
# Example:
# Creates named routes for implementing verb-oriented controllers
# for a collection resource.
#
# map.resources :messages
# For example:
#
# map.resources :messages
#
# will map the following actions in the corresponding controller:
#
# class MessagesController < ActionController::Base
# # GET messages_url
# def index
# # return all messages
# end
#
#
# # GET new_message_url
# def new
# # return an HTML form for describing a new message
# end
#
#
# # POST messages_url
# def create
# # create a new message
# end
#
#
# # GET message_url(:id => 1)
# def show
# # find and return a specific message
# end
#
#
# # GET edit_message_url(:id => 1)
# def edit
# # return an HTML form for editing a specific message
# end
#
#
# # PUT message_url(:id => 1)
# def update
# # find and update a specific message
# end
#
#
# # DELETE message_url(:id => 1)
# def destroy
# # delete a specific message
# end
# end
#
# The #resources method sets HTTP method restrictions on the routes it generates. For example, making an
# HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in
# <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes.
#
#
# Along with the routes themselves, #resources generates named routes for use in
# controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers:
#
#
# Named Route Helpers
# messages messages_url, hash_for_messages_url,
# ============ =====================================================
# messages messages_url, hash_for_messages_url,
# messages_path, hash_for_messages_path
# message message_url(id), hash_for_message_url(id),
#
# message message_url(id), hash_for_message_url(id),
# message_path(id), hash_for_message_path(id)
# new_message new_message_url, hash_for_new_message_url,
#
# new_message new_message_url, hash_for_new_message_url,
# new_message_path, hash_for_new_message_path
#
# edit_message edit_message_url(id), hash_for_edit_message_url(id),
# edit_message_path(id), hash_for_edit_message_path(id)
#
# You can use these helpers instead of #url_for or methods that take #url_for parameters:
#
# redirect_to :controller => 'messages', :action => 'index'
# # becomes
# redirect_to messages_url
# You can use these helpers instead of #url_for or methods that take #url_for parameters. For example:
#
# redirect_to :controller => 'messages', :action => 'index'
# # and
# <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %>
# # becomes
#
# now become:
#
# redirect_to messages_url
# # and
# <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically
#
# Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your
# form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object:
#
# <%= form_tag message_path(@message), :method => :put %>
#
# or
#
#
# or
#
# <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %>
#
# The #resources method accepts various options, too, to customize the resulting
# routes:
# * <tt>:controller</tt> -- specify the controller name for the routes.
# * <tt>:singular</tt> -- specify the singular name used in the member routes.
# * <tt>:path_prefix</tt> -- set a prefix to the routes with required route variables.
#
# The #resources method accepts the following options to customize the resulting routes:
# * <tt>:collection</tt> - add named routes for other actions that operate on the collection.
# Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>
# or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of rss_messages_url.
# * <tt>:member</tt> - same as :collection, but for actions that operate on a specific member.
# * <tt>:new</tt> - same as :collection, but for actions that operate on the new resource action.
# * <tt>:controller</tt> - specify the controller name for the routes.
# * <tt>:singular</tt> - specify the singular name used in the member routes.
# * <tt>:requirements</tt> - set custom routing parameter requirements.
# * <tt>:conditions</tt> - specify custom routing recognition conditions. Resources sets the :method value for the method-specific routes.
# * <tt>:path_prefix</tt> - set a prefix to the routes with required route variables.
#
# Weblog comments usually belong to a post, so you might use resources like:
#
# map.resources :articles
@ -198,85 +250,102 @@ module ActionController
# end
#
# The comment resources work the same, but must now include a value for :article_id.
#
#
# article_comments_url(@article)
# article_comment_url(@article, @comment)
#
# article_comments_url(:article_id => @article)
# article_comment_url(:article_id => @article, :id => @comment)
#
# * <tt>:name_prefix</tt> -- define a prefix for all generated routes, usually ending in an underscore.
# * <tt>:name_prefix</tt> - define a prefix for all generated routes, usually ending in an underscore.
# Use this if you have named routes that may clash.
#
# map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_'
# map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_'
#
# * <tt>:collection</tt> -- add named routes for other actions that operate on the collection.
# Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>
# or <tt>:any</tt> if the method does not matter. These routes map to a URL like /messages/rss, with a route of rss_messages_url.
# * <tt>:member</tt> -- same as :collection, but for actions that operate on a specific member.
# * <tt>:new</tt> -- same as :collection, but for actions that operate on the new resource action.
# You may also use :name_prefix to override the generic named routes in a nested resource:
#
# map.resources :articles do |article|
# article.resources :comments, :name_prefix => nil
# end
#
# This will yield named resources like so:
#
# comments_url(@article)
# comment_url(@article, @comment)
#
# If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
#
# Examples:
#
#
# map.resources :messages, :path_prefix => "/thread/:thread_id"
# # --> GET /thread/7/messages/1
#
#
# map.resources :messages, :collection => { :rss => :get }
# # --> GET /messages/rss (maps to the #rss action)
# # also adds a named route called "rss_messages"
#
#
# map.resources :messages, :member => { :mark => :post }
# # --> POST /messages/1/mark (maps to the #mark action)
# # also adds a named route called "mark_message"
#
#
# map.resources :messages, :new => { :preview => :post }
# # --> POST /messages/new/preview (maps to the #preview action)
# # also adds a named route called "preview_new_message"
#
#
# map.resources :messages, :new => { :new => :any, :preview => :post }
# # --> POST /messages/new/preview (maps to the #preview action)
# # also adds a named route called "preview_new_message"
# # --> /messages/new can be invoked via any request method
#
#
# map.resources :messages, :controller => "categories",
# :path_prefix => "/category/:category_id",
# :name_prefix => "category_"
# # --> GET /categories/7/messages/1
# # has named route "category_message"
#
# The #resources method sets HTTP method restrictions on the routes it generates. For example, making an
# HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in
# <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for resource routes.
def resources(*entities, &block)
options = entities.last.is_a?(Hash) ? entities.pop : { }
entities.each { |entity| map_resource entity, options.dup, &block }
options = entities.extract_options!
entities.each { |entity| map_resource(entity, options.dup, &block) }
end
# Creates named routes for implementing verb-oriented controllers for a singleton resource.
# A singleton resource is global to the current user visiting the application, such as a user's
# /account profile.
#
# Creates named routes for implementing verb-oriented controllers for a singleton resource.
# A singleton resource is global to its current context. For unnested singleton resources,
# the resource is global to the current user visiting the application, such as a user's
# /account profile. For nested singleton resources, the resource is global to its parent
# resource, such as a <tt>projects</tt> resource that <tt>has_one :project_manager</tt>.
# The <tt>project_manager</tt> should be mapped as a singleton resource under <tt>projects</tt>:
#
# map.resources :projects do |project|
# project.resource :project_manager
# end
#
# See map.resources for general conventions. These are the main differences:
# - A singular name is given to map.resource. The default controller name is taken from the singular name.
# - There is no <tt>:collection</tt> option as there is only the singleton resource.
# - There is no <tt>:singular</tt> option as the singular name is passed to map.resource.
# - No default index route is created for the singleton resource controller.
# - When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
# * A singular name is given to map.resource. The default controller name is still taken from the plural name.
# * To specify a custom plural name, use the :plural option. There is no :singular option.
# * No default index route is created for the singleton resource controller.
# * When nesting singleton resources, only the singular name is used as the path prefix (example: 'account/messages/1')
#
# Example:
# For example:
#
# map.resource :account
# map.resource :account
#
# class AccountController < ActionController::Base
# # POST account_url
# def create
# # create an account
# end
# maps these actions in the Accounts controller:
#
# class AccountsController < ActionController::Base
# # GET new_account_url
# def new
# # return an HTML form for describing the new account
# end
#
# # POST account_url
# def create
# # create an account
# end
#
# # GET account_url
# def show
# # find and return the account
@ -298,17 +367,23 @@ module ActionController
# end
# end
#
# Along with the routes themselves, #resource generates named routes for use in
# controllers and views. <tt>map.resource :account</tt> produces the following named routes and helpers:
#
# Along with the routes themselves, #resource generates named routes for
# use in controllers and views. <tt>map.resource :account</tt> produces
# these named routes and helpers:
#
# Named Route Helpers
# account account_url, hash_for_account_url,
# ============ =============================================
# account account_url, hash_for_account_url,
# account_path, hash_for_account_path
#
# new_account new_account_url, hash_for_new_account_url,
# new_account_path, hash_for_new_account_path
#
# edit_account edit_account_url, hash_for_edit_account_url,
# edit_account_path, hash_for_edit_account_path
def resource(*entities, &block)
options = entities.last.is_a?(Hash) ? entities.pop : { }
entities.each { |entity| map_singleton_resource entity, options.dup, &block }
options = entities.extract_options!
entities.each { |entity| map_singleton_resource(entity, options.dup, &block) }
end
private
@ -321,8 +396,10 @@ module ActionController
map_new_actions(map, resource)
map_member_actions(map, resource)
map_associations(resource, options)
if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, :new_name_prefix => resource.nesting_name_prefix, &block)
with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], &block)
end
end
end
@ -336,30 +413,32 @@ module ActionController
map_new_actions(map, resource)
map_member_actions(map, resource)
map_associations(resource, options)
if block_given?
with_options(:path_prefix => resource.nesting_path_prefix, :new_name_prefix => resource.nesting_name_prefix, &block)
with_options(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :namespace => options[:namespace], &block)
end
end
end
def map_associations(resource, options)
path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}"
name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}"
Array(options[:has_many]).each do |association|
resources(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace])
end
Array(options[:has_one]).each do |association|
resource(association, :path_prefix => path_prefix, :name_prefix => name_prefix, :namespace => options[:namespace])
end
end
def map_collection_actions(map, resource)
resource.collection_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
unless resource.old_name_prefix.blank?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.old_name_prefix}#{action}_#{resource.plural}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "formatted_#{resource.old_name_prefix}#{action}_#{resource.plural}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{action}_#{resource.plural}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "formatted_#{action}_#{resource.plural}")
end
map.named_route("#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}", action_options)
map.connect("#{resource.path};#{action}", action_options)
map.connect("#{resource.path}.:format;#{action}", action_options)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.plural}", "#{resource.path}#{resource.action_separator}#{action}.:format", action_options)
end
end
@ -367,14 +446,15 @@ module ActionController
def map_default_collection_actions(map, resource)
index_action_options = action_options_for("index", resource)
map.named_route("#{resource.name_prefix}#{resource.plural}", resource.path, index_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.plural}", "#{resource.path}.:format", index_action_options)
index_route_name = "#{resource.name_prefix}#{resource.plural}"
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{resource.name_prefix}#{resource.plural}", "#{resource.plural}")
map.deprecated_named_route("formatted_#{resource.name_prefix}#{resource.plural}", "formatted_#{resource.plural}")
if resource.uncountable?
index_route_name << "_index"
end
map.named_route(index_route_name, resource.path, index_action_options)
map.named_route("formatted_#{index_route_name}", "#{resource.path}.:format", index_action_options)
create_action_options = action_options_for("create", resource)
map.connect(resource.path, create_action_options)
map.connect("#{resource.path}.:format", create_action_options)
@ -391,37 +471,11 @@ module ActionController
actions.each do |action|
action_options = action_options_for(action, resource, method)
if action == :new
unless resource.old_name_prefix.blank?
map.deprecated_named_route("new_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}new_#{resource.singular}")
map.deprecated_named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}new_#{resource.singular}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("new_#{resource.name_prefix}#{resource.singular}", "new_#{resource.singular}")
map.deprecated_named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "formatted_new_#{resource.singular}")
end
map.named_route("new_#{resource.name_prefix}#{resource.singular}", resource.new_path, action_options)
map.named_route("formatted_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}.:format", action_options)
else
unless resource.old_name_prefix.blank?
map.deprecated_named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}#{action}_new_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}#{action}_new_#{resource.singular}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{action}_new_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "formatted_#{action}_new_#{resource.singular}")
end
map.named_route("#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}", action_options)
map.connect("#{resource.new_path};#{action}", action_options)
map.connect("#{resource.new_path}.:format;#{action}", action_options)
map.named_route("formatted_#{action}_new_#{resource.name_prefix}#{resource.singular}", "#{resource.new_path}#{resource.action_separator}#{action}.:format", action_options)
end
end
end
@ -431,22 +485,8 @@ module ActionController
resource.member_methods.each do |method, actions|
actions.each do |action|
action_options = action_options_for(action, resource, method)
unless resource.old_name_prefix.blank?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.old_name_prefix}#{action}_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.old_name_prefix}#{action}_#{resource.singular}")
end
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{action}_#{resource.singular}")
map.deprecated_named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "formatted_#{action}_#{resource.singular}")
end
map.named_route("#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}", action_options)
map.connect("#{resource.member_path};#{action}", action_options)
map.connect("#{resource.member_path}.:format;#{action}", action_options)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}.:format", action_options)
map.named_route("formatted_#{action}_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}#{resource.action_separator}#{action}.:format",action_options)
end
end
@ -454,11 +494,6 @@ module ActionController
map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)
if resource.deprecate_name_prefix?
map.deprecated_named_route("#{resource.name_prefix}#{resource.singular}", "#{resource.singular}")
map.deprecated_named_route("formatted_#{resource.name_prefix}#{resource.singular}", "formatted_#{resource.singular}")
end
update_action_options = action_options_for("update", resource)
map.connect(resource.member_path, update_action_options)
map.connect("#{resource.member_path}.:format", update_action_options)
@ -468,23 +503,27 @@ module ActionController
map.connect("#{resource.member_path}.:format", destroy_action_options)
end
def conditions_for(method)
{ :conditions => method == :any ? {} : { :method => method } }
def add_conditions_for(conditions, method)
returning({:conditions => conditions.dup}) do |options|
options[:conditions][:method] = method unless method == :any
end
end
def action_options_for(action, resource, method = nil)
default_options = { :action => action.to_s }
require_id = resource.kind_of?(SingletonResource) ? {} : { :requirements => { :id => Regexp.new("[^#{Routing::SEPARATORS.join}]+") } }
require_id = !resource.kind_of?(SingletonResource)
case default_options[:action]
when "index", "new" : default_options.merge(conditions_for(method || :get))
when "create" : default_options.merge(conditions_for(method || :post))
when "show", "edit" : default_options.merge(conditions_for(method || :get)).merge(require_id)
when "update" : default_options.merge(conditions_for(method || :put)).merge(require_id)
when "destroy" : default_options.merge(conditions_for(method || :delete)).merge(require_id)
else default_options.merge(conditions_for(method))
when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)
when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))
when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))
when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))
else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements)
end
end
end
end
ActionController::Routing::RouteSet::Mapper.send :include, ActionController::Resources
class ActionController::Routing::RouteSet::Mapper
include ActionController::Resources
end

View file

@ -1,6 +1,9 @@
require 'digest/md5'
module ActionController
class AbstractResponse #:nodoc:
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :request
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout
def initialize
@ -8,28 +11,66 @@ module ActionController
end
def content_type=(mime_type)
@headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type
self.headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type
end
def content_type
content_type = String(@headers["Content-Type"]).split(";")[0]
content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0]
content_type.blank? ? nil : content_type
end
def charset=(encoding)
@headers["Content-Type"] = "#{content_type || "text/html"}; charset=#{encoding}"
self.headers["Content-Type"] = "#{content_type || Mime::HTML}; charset=#{encoding}"
end
def charset
charset = String(@headers["Content-Type"]).split(";")[1]
charset = String(headers["Content-Type"] || headers["type"]).split(";")[1]
charset.blank? ? nil : charset.strip.split("=")[1]
end
def redirect(to_url, permanently = false)
@headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently"
@headers["Location"] = to_url
def redirect(to_url, response_status)
self.headers["Status"] = response_status
self.headers["Location"] = to_url
@body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
end
def prepare!
handle_conditional_get!
convert_content_type!
set_content_length!
end
private
def handle_conditional_get!
if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true) && !body.empty?
self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}")
self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
self.headers['Status'] = '304 Not Modified'
self.body = ''
end
end
end
def convert_content_type!
if content_type = headers.delete("Content-Type")
self.headers["type"] = content_type
end
if content_type = headers.delete("Content-type")
self.headers["type"] = content_type
end
if content_type = headers.delete("content-type")
self.headers["type"] = content_type
end
end
# Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
# for, say, a 2GB streaming file.
def set_content_length!
self.headers["Content-Length"] = body.size unless body.respond_to?(:call)
end
end
end

View file

@ -0,0 +1,119 @@
module ActionController
module Routing
# Much of the slow performance from routes comes from the
# complexity of expiry, :requirements matching, defaults providing
# and figuring out which url pattern to use. With named routes
# we can avoid the expense of finding the right route. So if
# they've provided the right number of arguments, and have no
# :requirements, we can just build up a string and return it.
#
# To support building optimisations for other common cases, the
# generation code is separated into several classes
module Optimisation
def generate_optimisation_block(route, kind)
return "" unless route.optimise?
OPTIMISERS.inject("") do |memo, klazz|
memo << klazz.new(route, kind).source_code
memo
end
end
class Optimiser
attr_reader :route, :kind
def initialize(route, kind)
@route = route
@kind = kind
end
def guard_condition
'false'
end
def generation_code
'nil'
end
def source_code
if applicable?
"return #{generation_code} if #{guard_condition}\n"
else
"\n"
end
end
# Temporarily disabled :url optimisation pending proper solution to
# Issues around request.host etc.
def applicable?
true
end
end
# Given a route:
# map.person '/people/:id'
#
# If the user calls person_url(@person), we can simply
# return a string like "/people/#{@person.to_param}"
# rather than triggering the expensive logic in url_for
class PositionalArguments < Optimiser
def guard_condition
number_of_arguments = route.segment_keys.size
# if they're using foo_url(:id=>2) it's one
# argument, but we don't want to generate /foos/id2
if number_of_arguments == 1
"defined?(request) && request && args.size == 1 && !args.first.is_a?(Hash)"
else
"defined?(request) && request && args.size == #{number_of_arguments}"
end
end
def generation_code
elements = []
idx = 0
if kind == :url
elements << '#{request.protocol}'
elements << '#{request.host_with_port}'
end
elements << '#{request.relative_url_root if request.relative_url_root}'
# The last entry in route.segments appears to # *always* be a
# 'divider segment' for '/' but we have assertions to ensure that
# we don't include the trailing slashes, so skip them.
(route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment|
if segment.is_a?(DynamicSegment)
elements << segment.interpolation_chunk("args[#{idx}].to_param")
idx += 1
else
elements << segment.interpolation_chunk
end
end
%("#{elements * ''}")
end
end
# This case is mostly the same as the positional arguments case
# above, but it supports additional query parameters as the last
# argument
class PositionalArgumentsWithAdditionalParams < PositionalArguments
def guard_condition
"defined?(request) && request && args.size == #{route.segment_keys.size + 1} && !args.last.has_key?(:anchor) && !args.last.has_key?(:port) && !args.last.has_key?(:host)"
end
# This case uses almost the same code as positional arguments,
# but add an args.last.to_query on the end
def generation_code
super.insert(-2, '?#{args.last.to_query}')
end
# To avoid generating http://localhost/?host=foo.example.com we
# can't use this optimisation on routes without any segments
def applicable?
super && route.segment_keys.size > 0
end
end
OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams]
end
end
end

View file

@ -1,189 +0,0 @@
module ActionController
module Scaffolding # :nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
# for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
# with both controller logic and default templates that through introspection already know which fields to display
# and which input types to use. Example:
#
# class WeblogController < ActionController::Base
# scaffold :entry
# end
#
# This tiny piece of code will add all of the following methods to the controller:
#
# class WeblogController < ActionController::Base
# verify :method => :post, :only => [ :destroy, :create, :update ],
# :redirect_to => { :action => :list }
#
# def index
# list
# end
#
# def list
# @entries = Entry.find(:all)
# render_scaffold "list"
# end
#
# def show
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
# def destroy
# Entry.find(params[:id]).destroy
# redirect_to :action => "list"
# end
#
# def new
# @entry = Entry.new
# render_scaffold
# end
#
# def create
# @entry = Entry.new(params[:entry])
# if @entry.save
# flash[:notice] = "Entry was successfully created"
# redirect_to :action => "list"
# else
# render_scaffold('new')
# end
# end
#
# def edit
# @entry = Entry.find(params[:id])
# render_scaffold
# end
#
# def update
# @entry = Entry.find(params[:id])
# @entry.attributes = params[:entry]
#
# if @entry.save
# flash[:notice] = "Entry was successfully updated"
# redirect_to :action => "show", :id => @entry
# else
# render_scaffold('edit')
# end
# end
# end
#
# The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
# and one action at a time while relying on the rest of the scaffolded templates and actions.
module ClassMethods
# Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
# one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
# and @post/@posts for the instance variables.
#
# It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
# make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
# instead of just list, show, and post. If suffix is used, then no index method is added.
def scaffold(model_id, options = {})
options.assert_valid_keys(:class_name, :suffix)
singular_name = model_id.to_s
class_name = options[:class_name] || singular_name.camelize
plural_name = singular_name.pluralize
suffix = options[:suffix] ? "_#{singular_name}" : ""
unless options[:suffix]
module_eval <<-"end_eval", __FILE__, __LINE__
def index
list
end
end_eval
end
module_eval <<-"end_eval", __FILE__, __LINE__
verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
:redirect_to => { :action => :list#{suffix} }
def list#{suffix}
@#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10
render#{suffix}_scaffold "list#{suffix}"
end
def show#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def destroy#{suffix}
#{class_name}.find(params[:id]).destroy
redirect_to :action => "list#{suffix}"
end
def new#{suffix}
@#{singular_name} = #{class_name}.new
render#{suffix}_scaffold
end
def create#{suffix}
@#{singular_name} = #{class_name}.new(params[:#{singular_name}])
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully created"
redirect_to :action => "list#{suffix}"
else
render#{suffix}_scaffold('new')
end
end
def edit#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
render#{suffix}_scaffold
end
def update#{suffix}
@#{singular_name} = #{class_name}.find(params[:id])
@#{singular_name}.attributes = params[:#{singular_name}]
if @#{singular_name}.save
flash[:notice] = "#{class_name} was successfully updated"
redirect_to :action => "show#{suffix}", :id => @#{singular_name}
else
render#{suffix}_scaffold('edit')
end
end
private
def render#{suffix}_scaffold(action=nil)
action ||= caller_method_name(caller)
# logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger
if template_exists?("\#{self.class.controller_path}/\#{action}")
render :action => action
else
@scaffold_class = #{class_name}
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
@scaffold_suffix = "#{suffix}"
add_instance_variables_to_assigns
@template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false))
if !active_layout.nil?
render :file => active_layout, :use_full_path => true
else
render :file => scaffold_path('layout')
end
end
end
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end
def caller_method_name(caller)
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
end
end_eval
end
end
end
end

View file

@ -122,13 +122,14 @@ class CGI
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
end
attr_writer :data
# Has the session been loaded yet?
def loaded?
!! @data
end
private
attr_writer :data
def marshal_data!
return false if !loaded?
@ -332,4 +333,4 @@ class CGI
end
end
end
end
end

View file

@ -0,0 +1,164 @@
require 'cgi'
require 'cgi/session'
require 'base64' # to convert Marshal.dump to ASCII
require 'openssl' # to generate the HMAC message digest
# This cookie-based session store is the Rails default. Sessions typically
# contain at most a user_id and flash message; both fit within the 4K cookie
# size limit. Cookie-based sessions are dramatically faster than the
# alternatives.
#
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
# TamperedWithCookie is raised if the data integrity check fails.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his user_id without knowing the secret key included in
# the hash. New apps are generated with a pregenerated secret in
# config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
# :secret An application-wide key string or block returning a string
# called per generated digest. The block is called with the
# CGI::Session instance as an argument. It's important that the
# secret is not vulnerable to a dictionary attack. Therefore,
# you should choose a secret consisting of random numbers and
# letters and more than 30 characters.
#
# Example: :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# :digest The message digest algorithm used to verify session integrity
# defaults to 'SHA1' but may be any digest provided by OpenSSL,
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# `rake secret` and set the key in config/environment.rb
#
# Note that changing digest or secret invalidates all existing sessions!
class CGI::Session::CookieStore
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
# Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end
# Raised when the cookie fails its integrity check.
class TamperedWithCookie < StandardError; end
# Called from CGI::Session only.
def initialize(session, options = {})
# The session_key option is required.
if options['session_key'].blank?
raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
end
# The secret option is required.
ensure_secret_secure(options['secret'])
# Keep the session and its secret on hand so we can read and write cookies.
@session, @secret = session, options['secret']
# Message digest defaults to SHA1.
@digest = options['digest'] || 'SHA1'
# Default cookie options derived from session settings.
@cookie_options = {
'name' => options['session_key'],
'path' => options['session_path'],
'domain' => options['session_domain'],
'expires' => options['session_expires'],
'secure' => options['session_secure']
}
# Set no_hidden and no_cookies since the session id is unused and we
# set our own data cookie.
options['no_hidden'] = true
options['no_cookies'] = true
end
# To prevent users from using something insecure like "Password" we make sure that the
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
# There's no way we can do this check if they've provided a proc for the
# secret.
return true if secret.is_a?(Proc)
if secret.blank?
raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
end
if secret.length < SECRET_MIN_LENGTH
raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
end
end
# Restore session data from the cookie.
def restore
@original = read_cookie
@data = unmarshal(@original) || {}
end
# Wait until close to write the session data cookie.
def update; end
# Write the session data cookie if it was loaded and has changed.
def close
if defined?(@data) && !@data.blank?
updated = marshal(@data)
raise CookieOverflow if updated.size > MAX
write_cookie('value' => updated) unless updated == @original
end
end
# Delete the session data by setting an expired cookie with no data.
def delete
@data = nil
clear_old_cookie_value
write_cookie('value' => '', 'expires' => 1.year.ago)
end
# Generate the HMAC keyed message digest. Uses SHA1 by default.
def generate_digest(data)
key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
end
private
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
data = Base64.encode64(Marshal.dump(session)).chop
CGI.escape "#{data}--#{generate_digest(data)}"
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
if cookie
data, digest = CGI.unescape(cookie).split('--')
unless digest == generate_digest(data)
delete
raise TamperedWithCookie
end
Marshal.load(Base64.decode64(data))
end
end
# Read the session data cookie.
def read_cookie
@session.cgi.cookies[@cookie_options['name']].first
end
# CGI likes to make you hack.
def write_cookie(options)
cookie = CGI::Cookie.new(@cookie_options.merge(options))
@session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
end
# Clear cookie value so subsequent new_session doesn't reload old data.
def clear_old_cookie_value
@session.cgi.cookies[@cookie_options['name']].clear
end
end

View file

@ -9,7 +9,7 @@
begin
require 'cgi/session'
require 'memcache'
require_library_or_gem 'memcache'
class CGI
class Session
@ -57,26 +57,22 @@ begin
@expires = options['expires'] || 0
@session_key = "session:#{id}"
@session_data = {}
# Add this key to the store if haven't done so yet
unless @cache.get(@session_key)
@cache.add(@session_key, @session_data, @expires)
end
end
# Restore session state from the session's memcache entry.
#
# Returns the session state as a hash.
def restore
begin
@session_data = @cache[@session_key] || {}
rescue
@session_data = {}
end
@session_data = @cache[@session_key] || {}
end
# Save session state to the session's memcache entry.
def update
begin
@cache.set(@session_key, @session_data, @expires)
rescue
# Ignore session update failures.
end
@cache.set(@session_key, @session_data, @expires)
end
# Update and close the session's memcache entry.
@ -86,17 +82,14 @@ begin
# Delete the session's memcache entry.
def delete
begin
@cache.delete(@session_key)
rescue
# Ignore session delete failures.
end
@cache.delete(@session_key)
@session_data = {}
end
def data
@session_data
end
end
end
end

View file

@ -1,3 +1,4 @@
require 'action_controller/session/cookie_store'
require 'action_controller/session/drb_store'
require 'action_controller/session/mem_cache_store'
if Object.const_defined?(:ActiveRecord)
@ -7,16 +8,17 @@ end
module ActionController #:nodoc:
module SessionManagement #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.send :alias_method_chain, :process, :session_management_support
base.send :alias_method_chain, :process_cleanup, :session_management_support
base.class_eval do
extend ClassMethods
alias_method_chain :process, :session_management_support
alias_method_chain :process_cleanup, :session_management_support
end
end
module ClassMethods
# Set the session store to be used for keeping the session data between requests. The default is using the
# file system, but you can also specify one of the other included stores (:active_record_store, :drb_store,
# :mem_cache_store, or :memory_store) or use your own class.
# Set the session store to be used for keeping the session data between requests. By default, sessions are stored
# in browser cookies (:cookie_store), but you can also specify one of the other included stores
# (:active_record_store, :p_store, drb_store, :mem_cache_store, or :memory_store) or your own custom class.
def session_store=(store)
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] =
store.is_a?(Symbol) ? CGI::Session.const_get(store == :drb_store ? "DRbStore" : store.to_s.camelize) : store
@ -61,10 +63,14 @@ module ActionController #:nodoc:
# session :off, :only => :foo,
# :if => Proc.new { |req| req.parameters[:ws] }
#
# # the session will be disabled for non html/ajax requests
# session :off,
# :if => Proc.new { |req| !(req.format.html? || req.format.js?) }
#
# All session options described for ActionController::Base.process_cgi
# are valid arguments.
def session(*args)
options = Hash === args.last ? args.pop : {}
options = args.extract_options!
options[:disabled] = true if !args.empty?
options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only]
@ -76,6 +82,9 @@ module ActionController #:nodoc:
write_inheritable_array("session_options", [options])
end
# So we can declare session options in the Rails initializer.
alias_method :session=, :session
def cached_session_options #:nodoc:
@session_options ||= read_inheritable_attribute("session_options") || []
end

View file

@ -29,6 +29,9 @@ module ActionController #:nodoc:
# * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
# Defaults to 4096.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
# * <tt>:url_based_filename</tt> - set to true if you want the browser guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# (setting :filename overrides this option).
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
@ -42,7 +45,7 @@ module ActionController #:nodoc:
# send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
#
# Show a 404 page in the browser:
# send_file '/path/to/404.html, :type => 'text/html; charset=utf-8', :status => 404
# send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description).
@ -59,7 +62,7 @@ module ActionController #:nodoc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
options[:length] ||= File.size(path)
options[:filename] ||= File.basename(path)
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
send_file_headers! options
@performed_render = false
@ -121,7 +124,7 @@ module ActionController #:nodoc:
headers.update(
'Content-Length' => options[:length],
'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers
'Content-Type' => options[:type].to_s.strip, # fixes a problem with extra '\r' with some browsers
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)

View file

@ -0,0 +1,24 @@
<% unless @exception.blamed_files.blank? %>
<% if (hide = @exception.blamed_files.length > 8) %>
<a href="#" onclick="document.getElementById('blame_trace').style.display='block'; return false;">Show blamed files</a>
<% end %>
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%=h @exception.describe_blame %></code></pre>
<% end %>
<%
clean_params = request.parameters.clone
clean_params.delete("action")
clean_params.delete("controller")
request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
%>
<h2 style="margin-top: 30px">Request</h2>
<p><b>Parameters</b>: <pre><%=h request_dump %></pre></p>
<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
<div id="session_dump" style="display:none"><%= debug(request.session.instance_variable_get("@data")) %></div>
<h2 style="margin-top: 30px">Response</h2>
<p><b>Headers</b>: <pre><%=h response ? response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p>

View file

@ -1,44 +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 %>
<% if false %>
<br /><br />
<% begin %>
<%= form_tag(request.request_uri, "method" => request.method) %>
<input type="hidden" name="BP-RETRY" value="1" />
<% for key, values in params %>
<% next if key == "BP-RETRY" %>
<% for value in Array(values) %>
<input type="hidden" name="<%= key %>" value="<%= value %>" />
<% end %>
<% end %>
<input type="submit" value="Retry with Breakpoint" />
</form>
<% rescue Exception => e %>
<%=h "Couldn't render breakpoint link due to #{e.class} #{e.message}" %>
<% end %>
<% end %>
<%
request_parameters_without_action = request.parameters.clone
request_parameters_without_action.delete("action")
request_parameters_without_action.delete("controller")
request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n")
%>
<h2 style="margin-top: 30px">Request</h2>
<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></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>
<b>Headers</b>: <%=h response ? response.headers.inspect.gsub(/,/, ",\n") : "None" %><br/>

View file

@ -6,6 +6,6 @@
</h1>
<pre><%=h @exception.clean_message %></pre>
<%= render_file(@rescues_path + "/_trace.rhtml", false) %>
<%= render_file(@rescues_path + "/_trace.erb", false) %>
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>
<%= render_file(@rescues_path + "/_request_and_response.erb", false) %>

View file

@ -15,7 +15,7 @@
<% @real_exception = @exception
@exception = @exception.original_exception || @exception %>
<%= render_file(@rescues_path + "/_trace.rhtml", false) %>
<%= render_file(@rescues_path + "/_trace.erb", false) %>
<% @exception = @real_exception %>
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>
<%= render_file(@rescues_path + "/_request_and_response.erb", false) %>

View file

@ -1,7 +0,0 @@
<h1>Editing <%= @scaffold_singular_name %></h1>
<%= error_messages_for(@scaffold_singular_name) %>
<%= form(@scaffold_singular_name, :action => "update#{@scaffold_suffix}") %>
<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View file

@ -1,69 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Scaffolding</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; }
.fieldWithErrors {
padding: 2px;
background-color: red;
display: table;
}
#errorExplanation {
width: 400px;
border: 2px solid red;
padding: 7px;
padding-bottom: 12px;
margin-bottom: 20px;
background-color: #f0f0f0;
}
#errorExplanation h2 {
text-align: left;
font-weight: bold;
padding: 5px 5px 5px 15px;
font-size: 12px;
margin: -7px;
background-color: #c00;
color: #fff;
}
#errorExplanation p {
color: #333;
margin-bottom: 0;
padding: 5px;
}
#errorExplanation ul li {
font-size: 12px;
list-style: square;
}
</style>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>

View file

@ -1,27 +0,0 @@
<h1>Listing <%= @scaffold_plural_name %></h1>
<table>
<tr>
<% for column in @scaffold_class.content_columns %>
<th><%= column.human_name %></th>
<% end %>
</tr>
<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
<tr>
<% for column in @scaffold_class.content_columns %>
<td><%= entry.send(column.name) %></td>
<% end %>
<td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry %></td>
<td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry %></td>
<td><%= link_to "Destroy", {:action => "destroy#{@scaffold_suffix}", :id => entry}, { :confirm => "Are you sure?", :method => :post } %></td>
</tr>
<% end %>
</table>
<%= link_to "Previous page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.previous %>
<%= link_to "Next page", { :page => instance_variable_get("@#{@scaffold_singular_name}_pages").current.next } if instance_variable_get("@#{@scaffold_singular_name}_pages").current.next %>
<br />
<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>

View file

@ -1,6 +0,0 @@
<h1>New <%= @scaffold_singular_name %></h1>
<%= error_messages_for(@scaffold_singular_name) %>
<%= form(@scaffold_singular_name, :action => "create#{@scaffold_suffix}") %>
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View file

@ -1,9 +0,0 @@
<% for column in @scaffold_class.content_columns %>
<p>
<b><%= column.human_name %>:</b>
<%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
</p>
<% end %>
<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}") %> |
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View file

@ -0,0 +1,53 @@
require 'active_support/test_case'
module ActionController
class NonInferrableControllerError < ActionControllerError
def initialize(name)
super "Unable to determine the controller to test from #{name}. " +
"You'll need to specify it using 'tests YourController' in your " +
"test case definition"
end
end
class TestCase < ActiveSupport::TestCase
@@controller_class = nil
class << self
def tests(controller_class)
self.controller_class = controller_class
end
def controller_class=(new_class)
prepare_controller_class(new_class)
write_inheritable_attribute(:controller_class, new_class)
end
def controller_class
if current_controller_class = read_inheritable_attribute(:controller_class)
current_controller_class
else
self.controller_class= determine_default_controller_class(name)
end
end
def determine_default_controller_class(name)
name.sub(/Test$/, '').constantize
rescue NameError
raise NonInferrableControllerError.new(name)
end
def prepare_controller_class(new_class)
new_class.class_eval do
def rescue_action(e)
raise e
end
end
end
end
def setup
@controller = self.class.controller_class.new
@request = TestRequest.new
@response = TestResponse.new
end
end
end

View file

@ -1,4 +1,4 @@
require File.dirname(__FILE__) + '/assertions'
require 'action_controller/assertions'
module ActionController #:nodoc:
class Base
@ -23,8 +23,7 @@ module ActionController #:nodoc:
class TestRequest < AbstractRequest #:nodoc:
attr_accessor :cookies, :session_options
attr_accessor :query_parameters, :request_parameters, :path, :session, :env
attr_accessor :host
attr_reader :request_uri_overridden
attr_accessor :host, :user_agent
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@query_parameters = query_parameters || {}
@ -41,18 +40,15 @@ module ActionController #:nodoc:
@session = TestSession.new
end
# Wraps raw_post in a StringIO.
def body
StringIO.new(raw_post)
end
# Either the RAW_POST_DATA environment variable or the URL-encoded request
# parameters.
def raw_post
if raw_post = env['RAW_POST_DATA']
raw_post
else
params = self.request_parameters.dup
%w(controller action only_path).each do |k|
params.delete(k)
params.delete(k.to_sym)
end
params.map { |k,v| [ CGI.escape(k.to_s), CGI.escape(v.to_s) ].join('=') }.sort.join('&')
end
env['RAW_POST_DATA'] ||= url_encoded_request_parameters
end
def port=(number)
@ -68,14 +64,12 @@ module ActionController #:nodoc:
# 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)
@request_uri_overridden = true
@env["REQUEST_URI"] = value
@request_uri = nil
@path = nil
end
def request_uri=(uri)
@env["REQUEST_URI"] = uri
@request_uri = uri
@path = uri.split("?").first
end
@ -93,11 +87,11 @@ module ActionController #:nodoc:
end
def request_uri
@request_uri || super()
@request_uri || super
end
def path
@path || super()
@path || super
end
def assign_parameters(controller_path, action, parameters)
@ -127,6 +121,10 @@ module ActionController #:nodoc:
@request_method, @accepts, @content_type = nil, nil, nil
end
def referer
@env["HTTP_REFERER"]
end
private
def initialize_containers
@env, @cookies = {}, {}
@ -135,10 +133,22 @@ module ActionController #:nodoc:
def initialize_default_values
@host = "test.host"
@request_uri = "/"
@user_agent = "Rails Testing"
self.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.to_query
end
end
# A refactoring of TestResponse to allow the same behavior to be applied
@ -260,13 +270,7 @@ module ActionController #:nodoc:
require 'stringio'
sio = StringIO.new
begin
$stdout = sio
body.call
ensure
$stdout = STDOUT
end
body.call(self, sio)
sio.rewind
sio.read
@ -320,33 +324,37 @@ module ActionController #:nodoc:
#
# Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
#
# 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(Test::Unit::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_reader :content_type
def initialize(path, content_type = 'text/plain')
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.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)
@tempfile.send!(method_name, *args, &block)
end
end
module TestProcess
def self.included(base)
# execute the request simulating a specific http method and set/volley the response
@ -365,7 +373,7 @@ module ActionController #:nodoc:
# Sanity check for required instance variables so we can give an
# understandable error message.
%w(@controller @request @response).each do |iv_name|
if !instance_variables.include?(iv_name) || instance_variable_get(iv_name).nil?
if !(instance_variables.include?(iv_name) || instance_variables.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
@ -388,7 +396,7 @@ module ActionController #:nodoc:
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*'
returning self.send(request_method, action, parameters, session, flash) do
returning send!(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
end
@ -429,17 +437,18 @@ module ActionController #:nodoc:
end
def build_request_uri(action, parameters)
unless @request.request_uri_overridden
options = @controller.send(:rewrite_options, 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)
@request.set_REQUEST_URI(url.rewrite(options))
end
end
def html_document
@html_document ||= HTML::Document.new(@response.body)
xml = @response.content_type =~ /xml$/
@html_document ||= HTML::Document.new(@response.body, false, xml)
end
def find_tag(conditions)
@ -451,16 +460,20 @@ module ActionController #:nodoc:
end
def method_missing(selector, *args)
return @controller.send(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
return @controller.send!(selector, *args) if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
return super
end
# Shortcut for ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type). Example:
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
def fixture_file_upload(path, mime_type = nil)
#
# To upload binary files on Windows, pass :binary as the last parameter. This will not affect other platforms.
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
def fixture_file_upload(path, mime_type = nil, binary = false)
ActionController::TestUploadedFile.new(
Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path,
mime_type
mime_type,
binary
)
end
@ -483,15 +496,15 @@ module ActionController #:nodoc:
#
def with_routing
real_routes = ActionController::Routing::Routes
ActionController::Routing.send :remove_const, :Routes
ActionController::Routing.module_eval { remove_const :Routes }
temporary_routes = ActionController::Routing::RouteSet.new
ActionController::Routing.send :const_set, :Routes, temporary_routes
ActionController::Routing.module_eval { const_set :Routes, temporary_routes }
yield temporary_routes
ensure
if ActionController::Routing.const_defined? :Routes
ActionController::Routing.send(:remove_const, :Routes)
ActionController::Routing.module_eval { remove_const :Routes }
end
ActionController::Routing.const_set(:Routes, real_routes) if real_routes
end

View file

@ -1,5 +1,4 @@
module ActionController
module ActionController
# Write URLs from arbitrary places in your codebase, such as your mailers.
#
# Example:
@ -15,59 +14,71 @@ module ActionController
#
# In addition to providing +url_for+, named routes are also accessible after
# including UrlWriter.
#
module UrlWriter
# The default options for urls written by this writer. Typically a :host pair
# is provided.
mattr_accessor :default_url_options
self.default_url_options = {}
def self.included(base) #:nodoc:
ActionController::Routing::Routes.named_routes.install base
ActionController::Routing::Routes.install_helpers base
base.mattr_accessor :default_url_options
base.default_url_options ||= default_url_options
end
# Generate a url with the provided options. The following special options may
# effect the constructed url:
# Generate a url based on the options provided, default_url_options and the
# routes defined in routes.rb. The following options are supported:
#
# * :host Specifies the host the link should be targetted at. This option
# must be provided either explicitly, or via default_url_options.
# * :protocol The protocol to connect to. Defaults to 'http'
# * :port Optionally specify the port to connect to.
# * <tt>:only_path</tt> If true, the relative url is returned. Defaults to false.
# * <tt>:protocol</tt> The protocol to connect to. Defaults to 'http'.
# * <tt>:host</tt> Specifies the host the link should be targetted at. If <tt>:only_path</tt> is false, this option must be
# provided either explicitly, or via default_url_options.
# * <tt>:port</tt> Optionally specify the port to connect to.
# * <tt>:anchor</tt> An anchor name to be appended to the path.
#
# Any other key(:controller, :action, etc...) given to <tt>url_for</tt> is forwarded to the Routes module.
#
# Examples:
#
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing'
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok'
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33'
#
def url_for(options)
options = self.class.default_url_options.merge(options)
url = ''
unless options.delete :only_path
url << (options.delete(:protocol) || 'http')
url << '://'
url << '://' unless url.match("://") #dont add separator if its already been specified in :protocol
raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
url << options.delete(:host)
url << ":#{options.delete(:port)}" if options.key?(:port)
else
# Delete the unused options to prevent their appearance in the query string
[:protocol, :host, :port].each { |k| options.delete k }
end
anchor = "##{options.delete(:anchor)}" if options.key?(:anchor)
anchor = "##{CGI.escape options.delete(:anchor).to_param.to_s}" if options.key?(:anchor)
url << Routing::Routes.generate(options, {})
return "#{url}#{anchor}"
end
url << anchor if anchor
return url
end
end
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
class UrlRewriter #:nodoc:
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root]
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root]
def initialize(request, parameters)
@request, @parameters = request, parameters
end
def rewrite(options = {})
rewrite_url(rewrite_path(options), options)
rewrite_url(options)
end
def to_str
@ -78,16 +89,20 @@ module ActionController
private
# Given a path and options, returns a rewritten URL string
def rewrite_url(path, options)
def rewrite_url(options)
rewritten_url = ""
unless options[:only_path]
rewritten_url << (options[:protocol] || @request.protocol)
rewritten_url << "://" unless rewritten_url.match("://")
rewritten_url << rewrite_authentication(options)
rewritten_url << (options[:host] || @request.host_with_port)
rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
end
path = rewrite_path(options)
rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << path
rewritten_url << '/' if options[:trailing_slash]
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
rewritten_url << "##{options[:anchor]}" if options[:anchor]
rewritten_url
@ -97,15 +112,24 @@ module ActionController
def rewrite_path(options)
options = options.symbolize_keys
options.update(options[:params].symbolize_keys) if options[:params]
if (overwrite = options.delete(:overwrite_params))
options.update(@parameters.symbolize_keys)
options.update(overwrite)
options.update(overwrite.symbolize_keys)
end
RESERVED_OPTIONS.each {|k| options.delete k}
RESERVED_OPTIONS.each { |k| options.delete(k) }
# Generates the query string, too
Routing::Routes.generate(options, @request.symbolized_path_parameters)
end
def rewrite_authentication(options)
if options[:user] && options[:password]
"#{CGI.escape(options.delete(:user))}:#{CGI.escape(options.delete(:password))}@"
else
""
end
end
end
end

View file

@ -1,9 +1,9 @@
require File.dirname(__FILE__) + '/tokenizer'
require File.dirname(__FILE__) + '/node'
require File.dirname(__FILE__) + '/selector'
require 'html/tokenizer'
require 'html/node'
require 'html/selector'
require 'html/sanitizer'
module HTML #:nodoc:
# A top-level HTMl document. You give it a body of text, and it will parse that
# text into a tree of nodes.
class Document #:nodoc:
@ -23,6 +23,9 @@ module HTML #:nodoc:
if node.tag?
if node_stack.length > 1 && node.closing == :close
if node_stack.last.name == node.name
if node_stack.last.children.empty?
node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
end
node_stack.pop
else
open_start = node_stack.last.position - 20

View file

@ -0,0 +1,173 @@
module HTML
class Sanitizer
def sanitize(text, options = {})
return text unless sanitizeable?(text)
tokenize(text, options).join
end
def sanitizeable?(text)
!(text.nil? || text.empty? || !text.index("<"))
end
protected
def tokenize(text, options)
tokenizer = HTML::Tokenizer.new(text)
result = []
while token = tokenizer.next
node = Node.parse(nil, 0, 0, token, false)
process_node node, result, options
end
result
end
def process_node(node, result, options)
result << node.to_s
end
end
class FullSanitizer < Sanitizer
def sanitize(text, options = {})
result = super
# strip any comments, and if they have a newline at the end (ie. line with
# only a comment) strip that too
result.gsub!(/<!--(.*?)-->[\n]?/m, "") if result
# Recurse - handle all dirty nested tags
result == text ? result : sanitize(result, options)
end
def process_node(node, result, options)
result << node.to_s if node.class == HTML::Text
end
end
class LinkSanitizer < FullSanitizer
cattr_accessor :included_tags, :instance_writer => false
self.included_tags = Set.new(%w(a href))
def sanitizeable?(text)
!(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
end
protected
def process_node(node, result, options)
result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
end
end
class WhiteListSanitizer < Sanitizer
[:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
:allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
class_inheritable_accessor attr, :instance_writer => false
end
# A regular expression of the valid characters used to separate protocols like
# the ':' in 'http://foo.com'
self.protocol_separator = /:|(&#0*58)|(&#x70)|(%|&#37;)3A/
# Specifies a Set of HTML attributes that can have URIs.
self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
# Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
# to just escaping harmless tags like &lt;font&gt;
self.bad_tags = Set.new(%w(script))
# 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
acronym a img blockquote del ins))
# Specifies the default Set of html attributes that the #sanitize helper will leave
# in the allowed tag.
self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
# Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
feed svn urn aim rsync tag ssh sftp rtsp afs))
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
border-color border-left-color border-right-color border-top-color clear color cursor direction display
elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
width))
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
# Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
self.shorthand_css_properties = Set.new(%w(background border margin padding))
# Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
def sanitize_css(style)
# disallow urls
style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
# gauntlet
if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ ||
style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$))*$/
return ''
end
clean = []
style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
if allowed_css_properties.include?(prop.downcase)
clean << prop + ': ' + val + ';'
elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
unless val.split().any? do |keyword|
!allowed_css_keywords.include?(keyword) &&
keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/
end
clean << prop + ': ' + val + ';'
end
end
end
clean.join(' ')
end
protected
def tokenize(text, options)
options[:parent] = []
options[:attributes] ||= allowed_attributes
options[:tags] ||= allowed_tags
super
end
def process_node(node, result, options)
result << case node
when HTML::Tag
if node.closing == :close
options[:parent].shift
else
options[:parent].unshift node.name
end
process_attributes_for node, options
options[:tags].include?(node.name) ? node : nil
else
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
end
end
def process_attributes_for(node, options)
return unless node.attributes
node.attributes.keys.each do |attr_name|
value = node.attributes[attr_name].to_s
if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
node.attributes.delete(attr_name)
else
node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(value)
end
end
end
def contains_bad_protocols?(attr_name, value)
uri_attributes.include?(attr_name) &&
(value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first))
end
end
end

View file

@ -240,19 +240,24 @@ module HTML
raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
@source = ""
values = values[0] if values.size == 1 && values[0].is_a?(Array)
# We need a copy to determine if we failed to parse, and also
# preserve the original pass by-ref statement.
statement = selector.strip.dup
# Create a simple selector, along with negation.
simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
@alternates = []
@depends = nil
# Alternative selector.
if statement.sub!(/^\s*,\s*/, "")
second = Selector.new(statement, values)
(@alternates ||= []) << second
@alternates << second
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, nil)
second.instance_variable_set(:@alternates, [])
@alternates.concat alternates
end
@source << " , " << second.to_s
@ -412,7 +417,7 @@ module HTML
# If this selector is part of the group, try all the alternative
# selectors (unless first_only).
if @alternates && (!first_only || !matches)
if !first_only || !matches
@alternates.each do |alternate|
break if matches && first_only
if subset = alternate.match(element, first_only)
@ -789,15 +794,15 @@ module HTML
# eventually, and array of substitution values.
#
# This method is called from four places, so it helps to put it here
# for resue. The only logic deals with the need to detect comma
# for reuse. The only logic deals with the need to detect comma
# separators (alternate) and apply them to the selector group of the
# top selector.
def next_selector(statement, values)
second = Selector.new(statement, values)
# If there are alternate selectors, we group them in the top selector.
if alternates = second.instance_variable_get(:@alternates)
second.instance_variable_set(:@alternates, nil)
(@alternates ||= []).concat alternates
second.instance_variable_set(:@alternates, [])
@alternates.concat alternates
end
second
end

View file

@ -1,97 +0,0 @@
require 'rexml/document'
# SimpleXML like xml parser. Written by leon breet from the ruby on rails Mailing list
class XmlNode #:nodoc:
attr :node
def initialize(node, options = {})
@node = node
@children = {}
@raise_errors = options[:raise_errors]
end
def self.from_xml(xml_or_io)
document = REXML::Document.new(xml_or_io)
if document.root
XmlNode.new(document.root)
else
XmlNode.new(document)
end
end
def node_encoding
@node.encoding
end
def node_name
@node.name
end
def node_value
@node.text
end
def node_value=(value)
@node.text = value
end
def xpath(expr)
matches = nil
REXML::XPath.each(@node, expr) do |element|
matches ||= XmlNodeList.new
matches << (@children[element] ||= XmlNode.new(element))
end
matches
end
def method_missing(name, *args)
name = name.to_s
nodes = nil
@node.each_element(name) do |element|
nodes ||= XmlNodeList.new
nodes << (@children[element] ||= XmlNode.new(element))
end
nodes
end
def <<(node)
if node.is_a? REXML::Node
child = node
elsif node.respond_to? :node
child = node.node
end
@node.add_element child
@children[child] ||= XmlNode.new(child)
end
def [](name)
@node.attributes[name.to_s]
end
def []=(name, value)
@node.attributes[name.to_s] = value
end
def to_s
@node.to_s
end
def to_i
to_s.to_i
end
end
class XmlNodeList < Array #:nodoc:
def [](i)
i.is_a?(String) ? super(0)[i] : super(i)
end
def []=(i, value)
i.is_a?(String) ? self[0][i] = value : super(i, value)
end
def method_missing(name, *args)
name = name.to_s
self[0].__send__(name, *args)
end
end

File diff suppressed because it is too large Load diff

View file

@ -12,7 +12,8 @@ module ActionController #:nodoc:
# parameters being set, or without certain session values existing.
#
# When a verification is violated, values may be inserted into the flash, and
# a specified redirection is triggered.
# a specified redirection is triggered. If no specific action is configured,
# verification failures will by default result in a 400 Bad Request response.
#
# Usage:
#
@ -42,37 +43,37 @@ module ActionController #:nodoc:
# the user is redirected to a different action. The +options+ parameter
# is a hash consisting of the following key/value pairs:
#
# * <tt>:params</tt>: a single key or an array of keys that must
# * <tt>:params</tt> - a single key or an array of keys that must
# be in the <tt>params</tt> hash in order for the action(s) to be safely
# called.
# * <tt>:session</tt>: a single key or an array of keys that must
# * <tt>:session</tt> - a single key or an array of keys that must
# be in the <tt>session</tt> in order for the action(s) to be safely called.
# * <tt>:flash</tt>: a single key or an array of keys that must
# * <tt>:flash</tt> - a single key or an array of keys that must
# be in the flash in order for the action(s) to be safely called.
# * <tt>:method</tt>: a single key or an array of keys--any one of which
# * <tt>:method</tt> - a single key or an array of keys--any one of which
# must match the current request method in order for the action(s) to
# be safely called. (The key should be a symbol: <tt>:get</tt> or
# <tt>:post</tt>, for example.)
# * <tt>:xhr</tt>: true/false option to ensure that the request is coming
# * <tt>:xhr</tt> - true/false option to ensure that the request is coming
# from an Ajax call or not.
# * <tt>:add_flash</tt>: a hash of name/value pairs that should be merged
# * <tt>:add_flash</tt> - a hash of name/value pairs that should be merged
# into the session's flash if the prerequisites cannot be satisfied.
# * <tt>:add_headers</tt>: a hash of name/value pairs that should be
# * <tt>:add_headers</tt> - a hash of name/value pairs that should be
# merged into the response's headers hash if the prerequisites cannot
# be satisfied.
# * <tt>:redirect_to</tt>: the redirection parameters to be used when
# * <tt>:redirect_to</tt> - the redirection parameters to be used when
# redirecting if the prerequisites cannot be satisfied. You can
# redirect either to named route or to the action in some controller.
# * <tt>:render</tt>: the render parameters to be used when
# * <tt>:render</tt> - the render parameters to be used when
# the prerequisites cannot be satisfied.
# * <tt>:only</tt>: only apply this verification to the actions specified
# * <tt>:only</tt> - only apply this verification to the actions specified
# in the associated array (may also be a single value).
# * <tt>:except</tt>: do not apply this verification to the actions
# * <tt>:except</tt> - do not apply this verification to the actions
# specified in the associated array (may also be a single value).
def verify(options={})
filter_opts = { :only => options[:only], :except => options[:except] }
before_filter(filter_opts) do |c|
c.send :verify_action, options
c.send! :verify_action, options
end
end
end
@ -81,7 +82,7 @@ module ActionController #:nodoc:
prereqs_invalid =
[*options[:params] ].find { |v| params[v].nil? } ||
[*options[:session]].find { |v| session[v].nil? } ||
[*options[:flash] ].find { |v| flash[v].nil? }
[*options[:flash] ].find { |v| flash[v].nil? }
if !prereqs_invalid && options[:method]
prereqs_invalid ||=
@ -93,16 +94,21 @@ module ActionController #:nodoc:
if prereqs_invalid
flash.update(options[:add_flash]) if options[:add_flash]
response.headers.update(options[:add_headers]) if options[:add_headers]
unless performed?
render(options[:render]) if options[:render]
options[:redirect_to] = self.send(options[:redirect_to]) if options[:redirect_to].is_a? Symbol
redirect_to(options[:redirect_to]) if options[:redirect_to]
end
return false
end
true
unless performed?
case
when options[:render]
render(options[:render])
when options[:redirect_to]
options[:redirect_to] = self.send!(options[:redirect_to]) if options[:redirect_to].is_a?(Symbol)
redirect_to(options[:redirect_to])
else
head(:bad_request)
end
end
end
end
private :verify_action
end
end
end