Vendoring Rails 2.3.5

This commit is contained in:
Eric Allen 2009-12-07 12:42:42 -05:00
parent 3e83d19299
commit f8779795ce
943 changed files with 56503 additions and 61351 deletions

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004-2008 David Heinemeier Hansson
# Copyright (c) 2004-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -31,49 +31,83 @@ rescue LoadError
end
end
$:.unshift "#{File.dirname(__FILE__)}/action_controller/vendor/html-scanner"
gem 'rack', '~> 1.0.1'
require 'rack'
require 'action_controller/cgi_ext'
require 'action_controller/base'
require 'action_controller/request'
require 'action_controller/rescue'
require 'action_controller/benchmarking'
require 'action_controller/flash'
require 'action_controller/filters'
require 'action_controller/layout'
require 'action_controller/mime_responds'
require 'action_controller/helpers'
require 'action_controller/cookies'
require 'action_controller/cgi_process'
require 'action_controller/caching'
require 'action_controller/verification'
require 'action_controller/streaming'
require 'action_controller/session_management'
require 'action_controller/http_authentication'
require 'action_controller/components'
require 'action_controller/rack_process'
require 'action_controller/record_identifier'
require 'action_controller/request_forgery_protection'
require 'action_controller/headers'
require 'action_controller/translation'
module ActionController
# TODO: Review explicit to see if they will automatically be handled by
# the initilizer if they are really needed.
def self.load_all!
[Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
end
autoload :Base, 'action_controller/base'
autoload :Benchmarking, 'action_controller/benchmarking'
autoload :Caching, 'action_controller/caching'
autoload :Cookies, 'action_controller/cookies'
autoload :Dispatcher, 'action_controller/dispatcher'
autoload :Failsafe, 'action_controller/failsafe'
autoload :Filters, 'action_controller/filters'
autoload :Flash, 'action_controller/flash'
autoload :Helpers, 'action_controller/helpers'
autoload :HttpAuthentication, 'action_controller/http_authentication'
autoload :Integration, 'action_controller/integration'
autoload :IntegrationTest, 'action_controller/integration'
autoload :Layout, 'action_controller/layout'
autoload :MiddlewareStack, 'action_controller/middleware_stack'
autoload :MimeResponds, 'action_controller/mime_responds'
autoload :ParamsParser, 'action_controller/params_parser'
autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes'
autoload :RecordIdentifier, 'action_controller/record_identifier'
autoload :Reloader, 'action_controller/reloader'
autoload :Request, 'action_controller/request'
autoload :RequestForgeryProtection, 'action_controller/request_forgery_protection'
autoload :Rescue, 'action_controller/rescue'
autoload :Resources, 'action_controller/resources'
autoload :Response, 'action_controller/response'
autoload :RewindableInput, 'action_controller/rewindable_input'
autoload :Routing, 'action_controller/routing'
autoload :SessionManagement, 'action_controller/session_management'
autoload :StatusCodes, 'action_controller/status_codes'
autoload :Streaming, 'action_controller/streaming'
autoload :StringCoercion, 'action_controller/string_coercion'
autoload :TestCase, 'action_controller/test_case'
autoload :TestProcess, 'action_controller/test_process'
autoload :Translation, 'action_controller/translation'
autoload :UploadedFile, 'action_controller/uploaded_file'
autoload :UploadedStringIO, 'action_controller/uploaded_file'
autoload :UploadedTempfile, 'action_controller/uploaded_file'
autoload :UrlRewriter, 'action_controller/url_rewriter'
autoload :UrlWriter, 'action_controller/url_rewriter'
autoload :Verification, 'action_controller/verification'
module Assertions
autoload :DomAssertions, 'action_controller/assertions/dom_assertions'
autoload :ModelAssertions, 'action_controller/assertions/model_assertions'
autoload :ResponseAssertions, 'action_controller/assertions/response_assertions'
autoload :RoutingAssertions, 'action_controller/assertions/routing_assertions'
autoload :SelectorAssertions, 'action_controller/assertions/selector_assertions'
autoload :TagAssertions, 'action_controller/assertions/tag_assertions'
end
module Http
autoload :Headers, 'action_controller/headers'
end
module Session
autoload :AbstractStore, 'action_controller/session/abstract_store'
autoload :CookieStore, 'action_controller/session/cookie_store'
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
end
# DEPRECATE: Remove CGI support
autoload :CgiRequest, 'action_controller/cgi_process'
autoload :CGIHandler, 'action_controller/cgi_process'
end
autoload :Mime, 'action_controller/mime_type'
autoload :HTML, 'action_controller/vendor/html-scanner'
require 'action_view'
ActionController::Base.class_eval do
include ActionController::Flash
include ActionController::Filters
include ActionController::Layout
include ActionController::Benchmarking
include ActionController::Rescue
include ActionController::MimeResponds
include ActionController::Helpers
include ActionController::Cookies
include ActionController::Caching
include ActionController::Verification
include ActionController::Streaming
include ActionController::SessionManagement
include ActionController::HttpAuthentication::Basic::ControllerMethods
include ActionController::Components
include ActionController::RecordIdentifier
include ActionController::RequestForgeryProtection
include ActionController::Translation
end

View file

@ -1,69 +0,0 @@
require 'test/unit/assertions'
module ActionController #:nodoc:
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * 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
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# 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 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.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
#
# For cookies, you need to manually create the cookie, like this:
#
# @request.cookies["key"] = CGI::Cookie.new("key", "value")
#
# == 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:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
def self.included(klass)
%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 => 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
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
include ActionController::Assertions
end
end
end

View file

@ -1,6 +1,18 @@
module ActionController
module Assertions
module DomAssertions
def self.strip_whitespace!(nodes)
nodes.reject! do |node|
if node.is_a?(HTML::Text)
node.content.strip!
node.content.empty?
else
strip_whitespace! node.children
false
end
end
end
# Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
#
# ==== Examples
@ -12,13 +24,15 @@ module ActionController
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)
DomAssertions.strip_whitespace!(expected_dom.children)
DomAssertions.strip_whitespace!(actual_dom.children)
full_message = build_message(message, "<?> expected but was\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+.
# The negated form of +assert_dom_equal+.
#
# ==== Examples
#
@ -29,8 +43,10 @@ module ActionController
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)
DomAssertions.strip_whitespace!(expected_dom.children)
DomAssertions.strip_whitespace!(actual_dom.children)
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

View file

@ -11,6 +11,7 @@ module ActionController
# assert_valid(model)
#
def assert_valid(record)
::ActiveSupport::Deprecation.warn("assert_valid is deprecated. Use assert record.valid? instead", caller)
clean_backtrace do
assert record.valid?, record.errors.full_messages.join("\n")
end

View file

@ -1,6 +1,3 @@
require 'rexml/document'
require 'html/document'
module ActionController
module Assertions
# A small suite of assertions that test responses from Rails applications.
@ -19,7 +16,7 @@ module ActionController
# ==== Examples
#
# # assert that the response was a redirection
# assert_response :redirect
# assert_response :redirect
#
# # assert that the response code was status code 401 (unauthorized)
# assert_response 401
@ -44,7 +41,7 @@ module ActionController
end
end
# Assert that the redirection options passed in match those of the redirect called in the latest action.
# 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.
#
@ -63,12 +60,17 @@ module ActionController
clean_backtrace do
assert_response(:redirect, message)
return true if options == @response.redirected_to
# Support partial arguments for hash redirections
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
return true if options.all? {|(key, value)| @response.redirected_to[key] == value}
if options.all? {|(key, value)| @response.redirected_to[key] == value}
callstack = caller.dup
callstack.slice!(0, 2)
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", callstack)
return true
end
end
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
options_after_normalisation = normalize_argument_to_redirection(options)
@ -78,29 +80,64 @@ module ActionController
end
end
# Asserts that the request was rendered with the appropriate template file.
# Asserts that the request was rendered with the appropriate template file or partials
#
# ==== Examples
#
# # assert that the "new" view template was rendered
# assert_template "new"
#
def assert_template(expected = nil, message=nil)
# # assert that the "new" view template was rendered with Symbol
# assert_template :new
#
# # assert that the "_customer" partial was rendered twice
# assert_template :partial => '_customer', :count => 2
#
# # assert that no partials were rendered
# assert_template :partial => false
#
def assert_template(options = {}, message = nil)
clean_backtrace do
rendered = @response.rendered_template.to_s
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do
if expected.nil?
@response.rendered_template.blank?
else
rendered.to_s.match(expected)
case options
when NilClass, String, Symbol
rendered = @response.rendered[:template].to_s
msg = build_message(message,
"expecting <?> but rendering with <?>",
options, rendered)
assert_block(msg) do
if options.nil?
@response.rendered[:template].blank?
else
rendered.to_s.match(options.to_s)
end
end
when Hash
if expected_partial = options[:partial]
partials = @response.rendered[:partials]
if expected_count = options[:count]
found = partials.detect { |p, _| p.to_s.match(expected_partial) }
actual_count = found.nil? ? 0 : found.second
msg = build_message(message,
"expecting ? to be rendered ? time(s) but rendered ? time(s)",
expected_partial, expected_count, actual_count)
assert(actual_count == expected_count.to_i, msg)
else
msg = build_message(message,
"expecting partial <?> but action rendered <?>",
options[:partial], partials.keys)
assert(partials.keys.any? { |p| p.to_s.match(expected_partial) }, msg)
end
else
assert @response.rendered[:partials].empty?,
"Expected no partials to be rendered"
end
else
raise ArgumentError
end
end
end
private
# Proxy to to_param if the object will respond to it.
def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value

View file

@ -134,7 +134,7 @@ module ActionController
path = "/#{path}" unless path.first == '/'
# Assume given controller
request = ActionController::TestRequest.new({}, {}, nil)
request = ActionController::TestRequest.new
request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path

View file

@ -3,9 +3,6 @@
# Under MIT and/or CC By license.
#++
require 'rexml/document'
require 'html/document'
module ActionController
module Assertions
unless const_defined?(:NO_STRIP)
@ -19,7 +16,7 @@ module ActionController
#
# Use +css_select+ to select elements without making an assertions, either
# from the response HTML or elements selected by the enclosing assertion.
#
#
# In addition to HTML responses, you can make the following assertions:
# * +assert_select_rjs+ - Assertions on HTML content of RJS update and insertion operations.
# * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
@ -27,6 +24,12 @@ module ActionController
#
# Also see HTML::Selector to learn how to use selectors.
module SelectorAssertions
def initialize(*args)
super
@selected = nil
end
# :call-seq:
# css_select(selector) => array
# css_select(element, selector) => array
@ -56,8 +59,8 @@ module ActionController
# end
#
# # Selects all list items in unordered lists
# items = css_select("ul>li")
#
# items = css_select("ul>li")
#
# # Selects all form tags and then all inputs inside the form
# forms = css_select("form")
# forms.each do |form|
@ -112,20 +115,27 @@ module ActionController
# starting from (and including) that element and all its children in
# depth-first order.
#
# If no element if specified, calling +assert_select+ will select from the
# response HTML. Calling #assert_select inside an +assert_select+ block will
# run the assertion for each element selected by the enclosing assertion.
# If no element if specified, calling +assert_select+ selects from the
# response HTML unless +assert_select+ is called from within an +assert_select+ block.
#
# When called with a block +assert_select+ passes an array of selected elements
# to the block. Calling +assert_select+ from the block, with no element specified,
# runs the assertion on the complete set of elements selected by the enclosing assertion.
# Alternatively the array may be iterated through so that +assert_select+ can be called
# separately for each element.
#
#
# ==== Example
# assert_select "ol>li" do |elements|
# If the response contains two ordered lists, each with four list elements then:
# assert_select "ol" do |elements|
# elements.each do |element|
# assert_select element, "li"
# assert_select element, "li", 4
# end
# end
#
# Or for short:
# assert_select "ol>li" do
# assert_select "li"
# will pass, as will:
# assert_select "ol" do
# assert_select "li", 8
# end
#
# The selector may be a CSS selector expression (String), an expression
@ -208,7 +218,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.
@ -221,7 +231,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
@ -311,10 +321,10 @@ 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)
@ -323,7 +333,7 @@ module ActionController
"at most #{max} #{pluralize['element', max]}"
end
end
# :call-seq:
# assert_select_rjs(id?) { |elements| ... }
# assert_select_rjs(statement, id?) { |elements| ... }
@ -340,7 +350,7 @@ 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 <tt>:replace</tt>, <tt>:replace_html</tt>,
# 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>.
#
@ -405,6 +415,7 @@ module ActionController
if rjs_type
if rjs_type == :insert
position = args.shift
id = args.shift
insertion = "insert_#{position}".to_sym
raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion]
statement = "(#{RJS_STATEMENTS[insertion]})"
@ -483,7 +494,7 @@ module ActionController
# end
# end
# end
#
#
#
# # Selects all paragraph tags from within the description of an RSS feed
# assert_select_feed :rss, 2.0 do
@ -590,7 +601,7 @@ module ActionController
def response_from_page_or_rjs()
content_type = @response.content_type
if content_type && content_type =~ /text\/javascript/
if content_type && Mime::JS =~ content_type
body = @response.body.dup
root = HTML::Node.new(nil)

View file

@ -1,6 +1,3 @@
require 'rexml/document'
require 'html/document'
module ActionController
module Assertions
# Pair of assertions to testing elements in the HTML output of the response.
@ -127,4 +124,4 @@ module ActionController
end
end
end
end
end

View file

@ -1,12 +1,3 @@
require 'action_controller/mime_type'
require 'action_controller/request'
require 'action_controller/response'
require 'action_controller/routing'
require 'action_controller/resources'
require 'action_controller/url_rewriter'
require 'action_controller/status_codes'
require 'action_view'
require 'drb'
require 'set'
module ActionController #:nodoc:
@ -31,7 +22,7 @@ module ActionController #:nodoc:
attr_reader :allowed_methods
def initialize(*allowed_methods)
super("Only #{allowed_methods.to_sentence} requests are allowed.")
super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
@allowed_methods = allowed_methods
end
@ -173,8 +164,8 @@ module ActionController #:nodoc:
#
# 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
# * ActiveRecord::SessionStore - 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 ActiveRecord::SessionStore, set
#
# config.action_controller.session_store = :active_record_store
#
@ -263,7 +254,7 @@ module ActionController #:nodoc:
cattr_reader :protected_instance_variables
# Controller specific instance variables which will not be accessible inside views.
@@protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller
@action_name @before_filter_chain_aborted @action_cache_path @_session @_cookies @_headers @_params
@action_name @before_filter_chain_aborted @action_cache_path @_session @_headers @_params
@_flash @_response)
# Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets,
@ -310,10 +301,7 @@ 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::MULTIPART_FORM => :multipart_form,
Mime::URL_ENCODED_FORM => :url_encoded_form,
Mime::XML => :xml_simple,
Mime::JSON => :json }
@@param_parsers = {}
cattr_accessor :param_parsers
# Controls the default charset for all renders.
@ -336,6 +324,10 @@ module ActionController #:nodoc:
# sets it to <tt>:authenticity_token</tt> by default.
cattr_accessor :request_forgery_protection_token
# Controls the IP Spoofing check when determining the remote IP.
@@ip_spoofing_check = true
cattr_accessor :ip_spoofing_check
# Indicates whether or not optimise the generated named
# route helper methods
cattr_accessor :optimise_named_routes
@ -387,6 +379,13 @@ module ActionController #:nodoc:
attr_accessor :action_name
class << self
def call(env)
# HACK: For global rescue to have access to the original request and response
request = env["action_controller.rescue.request"] ||= Request.new(env)
response = env["action_controller.rescue.response"] ||= Response.new
process(request, response)
end
# Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc:
new.process(request, response)
@ -492,9 +491,18 @@ module ActionController #:nodoc:
filtered_parameters[key] = '[FILTERED]'
elsif value.is_a?(Hash)
filtered_parameters[key] = filter_parameters(value)
elsif value.is_a?(Array)
filtered_parameters[key] = value.collect do |item|
case item
when Hash, Array
filter_parameters(item)
else
item
end
end
elsif block_given?
key = key.dup
value = value.dup if value
value = value.dup if value.duplicable?
yield key, value
filtered_parameters[key] = value
else
@ -507,7 +515,7 @@ module ActionController #:nodoc:
protected :filter_parameters
end
delegate :exempt_from_layout, :to => 'ActionView::Base'
delegate :exempt_from_layout, :to => 'ActionView::Template'
end
public
@ -529,7 +537,7 @@ module ActionController #:nodoc:
end
def send_response
response.prepare! unless component_request?
response.prepare!
response
end
@ -645,7 +653,7 @@ module ActionController #:nodoc:
end
def session_enabled?
request.session_options && request.session_options[:disabled] != false
ActiveSupport::Deprecation.warn("Sessions are now lazy loaded. So if you don't access them, consider them disabled.", caller)
end
self.view_paths = []
@ -785,9 +793,36 @@ module ActionController #:nodoc:
# # placed in "app/views/layouts/special.r(html|xml)"
# render :text => "Hi there!", :layout => "special"
#
# The <tt>:text</tt> 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
# done with this method can also be done more cleanly using one of the other rendering methods, most notably templates.
# === Streaming data and/or controlling the page generation
#
# The <tt>:text</tt> option can also accept a Proc object, which can be used to:
#
# 1. stream on-the-fly generated data to the browser. Note that you should
# use the methods provided by ActionController::Steaming instead if you
# want to stream a buffer or a file.
# 2. 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 done with this method can also be done more cleanly
# using one of the other rendering methods, most notably templates.
#
# Two arguments are passed to the proc, a <tt>response</tt> object and an
# <tt>output</tt> object. The response object is equivalent to the return
# value of the ActionController::Base#response method, and can be used to
# control various things in the HTTP response, such as setting the
# Content-Type header. The output object is an writable <tt>IO</tt>-like
# object, so one can call <tt>write</tt> and <tt>flush</tt> on it.
#
# The following example demonstrates how one can stream a large amount of
# on-the-fly generated data to the browser:
#
# # Streams about 180 MB of generated data to the browser.
# render :text => proc { |response, output|
# 10_000_000.times do |i|
# output.write("This is line #{i}\n")
# end
# }
#
# Another example:
#
# # Renders "Hello from code!"
# render :text => proc { |response, output| output.write("Hello from code!") }
@ -864,20 +899,31 @@ module ActionController #:nodoc:
def render(options = nil, extra_options = {}, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed?
validate_render_arguments(options, extra_options, block_given?)
if options.nil?
return render(:file => default_template_name, :layout => true)
elsif !extra_options.is_a?(Hash)
raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
else
if options == :update
options = extra_options.merge({ :update => true })
elsif !options.is_a?(Hash)
raise RenderError, "You called render with invalid options : #{options.inspect}"
options = { :template => default_template, :layout => true }
elsif options == :update
options = extra_options.merge({ :update => true })
elsif options.is_a?(String) || options.is_a?(Symbol)
case options.to_s.index('/')
when 0
extra_options[:file] = options
when nil
extra_options[:action] = options
else
extra_options[:template] = options
end
options = extra_options
elsif !options.is_a?(Hash)
extra_options[:partial] = options
options = extra_options
end
response.layout = layout = pick_layout(options)
logger.info("Rendering template within #{layout}") if logger && layout
layout = pick_layout(options)
response.layout = layout.path_without_format_and_extension if layout
logger.info("Rendering template within #{layout.path_without_format_and_extension}") if logger && layout
if content_type = options[:content_type]
response.content_type = content_type.to_s
@ -902,7 +948,7 @@ module ActionController #:nodoc:
render_for_text(@template.render(options.merge(:layout => layout)), options[:status])
elsif action_name = options[:action]
render_for_file(default_template_name(action_name.to_s), options[:status], layout)
render_for_file(default_template(action_name.to_s), options[:status], layout)
elsif xml = options[:xml]
response.content_type ||= Mime::XML
@ -912,8 +958,9 @@ module ActionController #:nodoc:
response.content_type ||= Mime::JS
render_for_text(js, options[:status])
elsif json = options[:json]
json = json.to_json unless json.is_a?(String)
elsif options.include?(:json)
json = options[:json]
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
response.content_type ||= Mime::JSON
render_for_text(json, options[:status])
@ -937,7 +984,7 @@ module ActionController #:nodoc:
render_for_text(nil, options[:status])
else
render_for_file(default_template_name, options[:status], layout)
render_for_file(default_template, options[:status], layout)
end
end
end
@ -994,7 +1041,7 @@ module ActionController #:nodoc:
@performed_redirect = false
response.redirected_to = nil
response.redirected_to_method_params = nil
response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE
response.status = DEFAULT_RENDER_STATUS_CODE
response.headers.delete('Location')
end
@ -1065,7 +1112,6 @@ module ActionController #:nodoc:
end
response.redirected_to = options
logger.info("Redirected to #{options}") if logger && logger.info?
case options
# The scheme name consist of a letter followed by any combination of
@ -1088,6 +1134,7 @@ module ActionController #:nodoc:
def redirect_to_full_url(url, status)
raise DoubleRenderError if performed?
logger.info("Redirected to #{url}") if logger && logger.info?
response.redirect(url, interpret_status(status))
@performed_redirect = true
end
@ -1097,6 +1144,11 @@ module ActionController #:nodoc:
# request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
#
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example:
#
# def show
@ -1115,22 +1167,36 @@ module ActionController #:nodoc:
end
# Sets the etag, last_modified, or both on the response and renders a
# "304 Not Modified" response if the request is already fresh.
# "304 Not Modified" response if the request is already fresh.
#
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc)
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true)
# end
#
# This will render the show template if the request isn't sending a matching etag or
#
# This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match.
#
def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified)
options.assert_valid_keys(:etag, :last_modified, :public)
response.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified]
if options[:public]
cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
cache_control.delete("private")
cache_control.delete("no-cache")
cache_control << "public"
response.headers["Cache-Control"] = cache_control.join(', ')
end
if request.fresh?(response)
head :not_modified
@ -1142,15 +1208,26 @@ module ActionController #:nodoc:
#
# Examples:
# expires_in 20.minutes
# expires_in 3.hours, :private => false
# expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true
# expires_in 3.hours, :public => true
# expires in 3.hours, 'max-stale' => 5.hours, :public => true
#
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc:
cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys)
cache_options.delete_if { |k,v| v.nil? or v == false }
cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
cache_control << "max-age=#{seconds}"
cache_control.delete("no-cache")
if options[:public]
cache_control.delete("private")
cache_control << "public"
else
cache_control << "private"
end
# This allows for additional headers to be passed through like 'max-stale' => 5.hours
cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
response.headers["Cache-Control"] = cache_control.join(', ')
end
@ -1164,20 +1241,19 @@ module ActionController #:nodoc:
def reset_session #:doc:
request.reset_session
@_session = request.session
response.session = @_session
end
private
def render_for_file(template_path, status = nil, layout = nil, locals = {}) #:nodoc:
logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
path = template_path.respond_to?(:path_without_format_and_extension) ? template_path.path_without_format_and_extension : template_path
logger.info("Rendering #{path}" + (status ? " (#{status})" : '')) if logger
render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status
end
def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true
response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
response.status = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
response.body ||= ''
@ -1191,6 +1267,16 @@ module ActionController #:nodoc:
end
end
def validate_render_arguments(options, extra_options, has_block)
if options && (has_block && options != :update) && !options.is_a?(String) && !options.is_a?(Hash) && !options.is_a?(Symbol)
raise RenderError, "You called render with invalid options : #{options.inspect}"
end
if !extra_options.is_a?(Hash)
raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
end
end
def initialize_template_class(response)
response.template = ActionView::Base.new(self.class.view_paths, {}, self)
response.template.helpers.send :include, self.class.master_helper_module
@ -1199,7 +1285,7 @@ module ActionController #:nodoc:
end
def assign_shortcuts(request, response)
@_request, @_params, @_cookies = request, request.parameters, request.cookies
@_request, @_params = request, request.parameters
@_response = response
@_response.session = request.session
@ -1217,11 +1303,10 @@ module ActionController #:nodoc:
def log_processing
if logger && logger.info?
log_processing_for_request_id
log_processing_for_session_id
log_processing_for_parameters
end
end
def log_processing_for_request_id
request_id = "\n\nProcessing #{self.class.name}\##{action_name} "
request_id << "to #{params[:format]} " if params[:format]
@ -1230,17 +1315,10 @@ module ActionController #:nodoc:
logger.info(request_id)
end
def log_processing_for_session_id
if @_session && @_session.respond_to?(:session_id) && @_session.respond_to?(:dbman) &&
!@_session.dbman.is_a?(CGI::Session::CookieStore)
logger.info " Session ID: #{@_session.session_id}"
end
end
def log_processing_for_parameters
parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
parameters = parameters.except!(:controller, :action, :format, :_method)
logger.info " Parameters: #{parameters.inspect}" unless parameters.empty?
end
@ -1255,10 +1333,17 @@ module ActionController #:nodoc:
elsif respond_to? :method_missing
method_missing action_name
default_render unless performed?
elsif template_exists?
default_render
else
raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
begin
default_render
rescue ActionView::MissingTemplate => e
# Was the implicit template missing, or was it another template?
if e.path == default_template_name
raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence(:locale => :en)}", caller
else
raise e
end
end
end
end
@ -1270,11 +1355,6 @@ module ActionController #:nodoc:
@action_name = (params['action'] || 'index')
end
def assign_default_content_type_and_charset
response.assign_default_content_type_and_charset!
end
deprecate :assign_default_content_type_and_charset => :'response.assign_default_content_type_and_charset!'
def action_methods
self.class.action_methods
end
@ -1305,14 +1385,8 @@ module ActionController #:nodoc:
"#{request.protocol}#{request.host}#{request.request_uri}"
end
def close_session
@_session.close if @_session && @_session.respond_to?(:close)
end
def template_exists?(template_name = default_template_name)
@template.send(:_pick_template, template_name) ? true : false
rescue ActionView::MissingTemplate
false
def default_template(action_name = self.action_name)
self.view_paths.find_template(default_template_name(action_name), default_template_format)
end
def default_template_name(action_name = self.action_name)
@ -1334,7 +1408,16 @@ module ActionController #:nodoc:
end
def process_cleanup
close_session
end
end
Base.class_eval do
[ Filters, Layout, Benchmarking, Rescue, Flash, MimeResponds, Helpers,
Cookies, Caching, Verification, Streaming, SessionManagement,
HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods,
RecordIdentifier, RequestForgeryProtection, Translation
].each do |mod|
include mod
end
end
end

View file

@ -23,8 +23,8 @@ module ActionController #:nodoc:
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
if logger && logger.level == log_level
result = nil
seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
logger.add(log_level, "#{title} (#{('%.1f' % (seconds * 1000))}ms)")
ms = Benchmark.ms { result = use_silence ? silence { yield } : yield }
logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)")
result
else
yield
@ -48,7 +48,7 @@ module ActionController #:nodoc:
end
render_output = nil
@view_runtime = Benchmark::realtime { render_output = render_without_benchmark(options, extra_options, &block) }
@view_runtime = Benchmark.ms { render_output = render_without_benchmark(options, extra_options, &block) }
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@ -65,11 +65,11 @@ module ActionController #:nodoc:
private
def perform_action_with_benchmark
if logger
seconds = [ Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001 ].max
ms = [Benchmark.ms { perform_action_without_benchmark }, 0.01].max
logging_view = defined?(@view_runtime)
logging_active_record = Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
log_message = "Completed in #{sprintf("%.0f", seconds * 1000)}ms"
log_message = 'Completed in %.0fms' % ms
if logging_view || logging_active_record
log_message << " ("
@ -83,25 +83,25 @@ module ActionController #:nodoc:
end
end
log_message << " | #{headers["Status"]}"
log_message << " | #{response.status}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
response.headers["X-Runtime"] = "#{sprintf("%.0f", seconds * 1000)}ms"
response.headers["X-Runtime"] = "%.0f" % ms
else
perform_action_without_benchmark
end
end
def view_runtime
"View: %.0f" % (@view_runtime * 1000)
"View: %.0f" % @view_runtime
end
def active_record_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: %.0f" % (db_runtime * 1000)
"DB: %.0f" % db_runtime
end
end
end

View file

@ -2,13 +2,6 @@ require 'fileutils'
require 'uri'
require 'set'
require 'action_controller/caching/pages'
require 'action_controller/caching/actions'
require 'action_controller/caching/sql_cache'
require 'action_controller/caching/sweeping'
require 'action_controller/caching/fragments'
module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
# around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment.
@ -29,8 +22,15 @@ module ActionController #:nodoc:
# ActionController::Base.cache_store = :file_store, "/path/to/cache/directory"
# ActionController::Base.cache_store = :drb_store, "druby://localhost:9192"
# ActionController::Base.cache_store = :mem_cache_store, "localhost"
# ActionController::Base.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211")
# ActionController::Base.cache_store = MyOwnStore.new("parameter")
module Caching
autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages'
autoload :Sweeper, 'action_controller/caching/sweeper'
autoload :Sweeping, 'action_controller/caching/sweeping'
def self.included(base) #:nodoc:
base.class_eval do
@@cache_store = nil
@ -42,7 +42,7 @@ module ActionController #:nodoc:
end
include Pages, Actions, Fragments
include Sweeping, SqlCache if defined?(ActiveRecord)
include Sweeping if defined?(ActiveRecord)
@@perform_caching = true
cattr_accessor :perform_caching
@ -63,10 +63,9 @@ module ActionController #:nodoc:
end
end
private
private
def cache_configured?
self.class.cache_configured?
end
end
end
end

View file

@ -61,7 +61,9 @@ module ActionController #:nodoc:
filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) }
cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options)
around_filter(cache_filter, filter_options)
around_filter(filter_options) do |controller, action|
cache_filter.filter(controller, action)
end
end
end
@ -83,6 +85,12 @@ module ActionController #:nodoc:
@options = options
end
def filter(controller, action)
should_continue = before(controller)
action.call if should_continue
after(controller)
end
def before(controller)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
if cache = controller.read_fragment(cache_path.path, @options[:store_options])
@ -113,7 +121,7 @@ module ActionController #:nodoc:
end
def caching_allowed(controller)
controller.request.get? && controller.response.headers['Status'].to_i == 200
controller.request.get? && controller.response.status.to_i == 200
end
def cache_layout?
@ -129,24 +137,23 @@ module ActionController #:nodoc:
attr_reader :path, :extension
class << self
def path_for(controller, options, infer_extension=true)
def path_for(controller, options, infer_extension = true)
new(controller, options, infer_extension).path
end
end
# When true, infer_extension will look up the cache path extension from the request's path & format.
# This is desirable when reading and writing the cache, but not when expiring the cache - expire_action should expire the same files regardless of the request format.
def initialize(controller, options = {}, infer_extension=true)
if infer_extension and options.is_a? Hash
request_extension = extract_extension(controller.request)
options = options.reverse_merge(:format => request_extension)
# This is desirable when reading and writing the cache, but not when expiring the cache -
# expire_action should expire the same files regardless of the request format.
def initialize(controller, options = {}, infer_extension = true)
if infer_extension
extract_extension(controller.request)
options = options.reverse_merge(:format => @extension) if options.is_a?(Hash)
end
path = controller.url_for(options).split('://').last
normalize!(path)
if infer_extension
@extension = request_extension
add_extension!(path, @extension)
end
add_extension!(path, @extension)
@path = URI.unescape(path)
end
@ -162,13 +169,7 @@ module ActionController #:nodoc:
def extract_extension(request)
# Don't want just what comes after the last '.' to accommodate multi part extensions
# such as tar.gz.
extension = request.path[/^[^.]+\.(.+)$/, 1]
# If there's no extension in the path, check request.format
if extension.nil?
extension = request.cache_format
end
extension
@extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format
end
end
end

View file

@ -10,23 +10,23 @@ module ActionController #:nodoc:
# <%= render :partial => "topic", :collection => Topic.find(:all) %>
# <% end %>
#
# 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
# 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", 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.
#
# 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")
module Fragments
# Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
# Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
# writing, or expiring a cached fragment. If the key is a hash, the generated key is the return
# value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses
# ActiveSupport::Cache.expand_cache_key for the expansion.
@ -50,7 +50,7 @@ module ActionController #:nodoc:
# Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
def write_fragment(key, content, options = nil)
return unless cache_configured?
return content unless cache_configured?
key = fragment_cache_key(key)
@ -83,15 +83,23 @@ module ActionController #:nodoc:
end
end
# Name can take one of three forms:
# * String: This would normally take the form of a path like "pages/45/notes"
# * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
# * Regexp: Will destroy all the matched fragments, example:
# %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 only supported on caches that can iterate over
# all keys (unlike memcached).
# Removes fragments from the cache.
#
# +key+ can take one of three forms:
# * String - This would normally take the form of a path, like
# <tt>"pages/45/notes"</tt>.
# * Hash - Treated as an implicit call to +url_for+, like
# <tt>{:controller => "pages", :action => "notes", :id => 45}</tt>
# * Regexp - Will remove any fragment that matches, so
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
# the actual filename matched looks like
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
# only supported on caches that can iterate over all keys (unlike
# memcached).
#
# +options+ is passed through to the cache store's <tt>delete</tt>
# method (or <tt>delete_matched</tt>, for Regexp keys.)
def expire_fragment(key, options = nil)
return unless cache_configured?

View file

@ -33,28 +33,26 @@ module ActionController #:nodoc:
#
# Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
# expired.
#
# == Setting the cache directory
#
# The cache directory should be the document root for the web server and is set using <tt>Base.page_cache_directory = "/document/root"</tt>.
# For Rails, this directory has already been set to Rails.public_path (which is usually set to <tt>RAILS_ROOT + "/public"</tt>). Changing
# this setting can be useful to avoid naming conflicts with files in <tt>public/</tt>, but doing so will likely require configuring your
# web server to look in the new location for cached files.
#
# == Setting the cache extension
#
# Most Rails requests do not have an extension, such as <tt>/weblog/new</tt>. In these cases, the page caching mechanism will add one in
# order to make it easy for the cached files to be picked up properly by the web server. By default, this cache extension is <tt>.html</tt>.
# If you want something else, like <tt>.php</tt> or <tt>.shtml</tt>, just set Base.page_cache_extension. In cases where a request already has an
# extension, such as <tt>.xml</tt> or <tt>.rss</tt>, page caching will not add an extension. This allows it to work well with RESTful apps.
module Pages
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
@@page_cache_directory = defined?(Rails.public_path) ? Rails.public_path : ""
##
# :singleton-method:
# The cache directory should be the document root for the web server and is set using <tt>Base.page_cache_directory = "/document/root"</tt>.
# For Rails, this directory has already been set to Rails.public_path (which is usually set to <tt>RAILS_ROOT + "/public"</tt>). Changing
# this setting can be useful to avoid naming conflicts with files in <tt>public/</tt>, but doing so will likely require configuring your
# web server to look in the new location for cached files.
cattr_accessor :page_cache_directory
@@page_cache_extension = '.html'
##
# :singleton-method:
# Most Rails requests do not have an extension, such as <tt>/weblog/new</tt>. In these cases, the page caching mechanism will add one in
# order to make it easy for the cached files to be picked up properly by the web server. By default, this cache extension is <tt>.html</tt>.
# If you want something else, like <tt>.php</tt> or <tt>.shtml</tt>, just set Base.page_cache_extension. In cases where a request already has an
# extension, such as <tt>.xml</tt> or <tt>.rss</tt>, page caching will not add an extension. This allows it to work well with RESTful apps.
cattr_accessor :page_cache_extension
end
end
@ -147,7 +145,7 @@ module ActionController #:nodoc:
private
def caching_allowed
request.get? && response.headers['Status'].to_i == 200
request.get? && response.status.to_i == 200
end
end
end

View file

@ -1,18 +0,0 @@
module ActionController #:nodoc:
module Caching
module SqlCache
def self.included(base) #:nodoc:
if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache)
base.alias_method_chain :perform_action, :caching
end
end
protected
def perform_action_with_caching
ActiveRecord::Base.cache do
perform_action_without_caching
end
end
end
end
end

View file

@ -0,0 +1,45 @@
require 'active_record'
module ActionController #:nodoc:
module Caching
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
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, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end

View file

@ -51,47 +51,5 @@ module ActionController #:nodoc:
end
end
end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
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, 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)
end
end
end
end
end

View file

@ -1,7 +1,6 @@
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

View file

@ -1,3 +1,5 @@
require 'delegate'
CGI.module_eval { remove_const "Cookie" }
# TODO: document how this differs from stdlib CGI::Cookie

View file

@ -1,53 +0,0 @@
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:
def self.generate_unique_id(constant = nil)
ActiveSupport::SecureRandom.hex(16)
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,185 +1,77 @@
require 'action_controller/cgi_ext'
require 'action_controller/session/cookie_store'
module ActionController #:nodoc:
class Base
# Process a request extracted from a CGI object and return a response. Pass false as <tt>session_options</tt> to disable
# sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
#
# * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# lib/action_controller/session.
# * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
# * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ cookie, or
# automatically generated for a new session.
# * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
# an ArgumentError is raised.
# * <tt>:session_expires</tt> - the time the current session expires, as a Time object. If not set, the session will continue
# indefinitely.
# * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
# server.
# * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
# * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
# * <tt>:cookie_only</tt> - if +true+ (the default), session IDs will only be accepted from cookies and not from
# the query string or POST parameters. This protects against session fixation attacks.
def self.process_cgi(cgi = CGI.new, session_options = {})
new.process_cgi(cgi, session_options)
end
def process_cgi(cgi, session_options = {}) #:nodoc:
process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
end
end
class CgiRequest < AbstractRequest #:nodoc:
attr_accessor :cgi, :session_options
class SessionFixationAttempt < StandardError #:nodoc:
end
DEFAULT_SESSION_OPTIONS = {
: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,
:session_http_only=> true
}
def initialize(cgi, session_options = {})
@cgi = cgi
@session_options = session_options
@env = @cgi.__send__(:env_table)
super()
end
def query_string
qs = @cgi.query_string if @cgi.respond_to?(:query_string)
if !qs.blank?
qs
else
super
end
end
def body_stream #:nodoc:
@cgi.stdinput
end
def cookies
@cgi.cookies.freeze
end
def session
unless defined?(@session)
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
raise SessionFixationAttempt
end
case value = session_options_with_string_keys['new_session']
when true
@session = new_session
when false
begin
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
# CGI::Session raises ArgumentError if 'new_session' == false
# and no session cookie or query param is present.
rescue ArgumentError
@session = Hash.new
end
when nil
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
else
raise ArgumentError, "Invalid new_session option: #{value}"
end
@session['__valid_session']
end
end
end
@session
end
def reset_session
@session.delete if defined?(@session) && @session.is_a?(CGI::Session)
@session = new_session
end
def method_missing(method_id, *arguments)
@cgi.__send__(method_id, *arguments) rescue super
end
private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
class CGIHandler
module ProperStream
def each
while line = gets
yield line
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:]*\w)}
begin
# 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.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
end
retry
def read(*args)
if args.empty?
super || ""
else
raise
super
end
end
def session_options_with_string_keys
@session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
end
end
class CgiResponse < AbstractResponse #:nodoc:
def initialize(cgi)
@cgi = cgi
super()
end
def out(output = $stdout)
output.binmode if output.respond_to?(:binmode)
output.sync = false if output.respond_to?(:sync=)
def self.dispatch_cgi(app, cgi, out = $stdout)
env = cgi.__send__(:env_table)
env.delete "HTTP_CONTENT_LENGTH"
cgi.stdinput.extend ProperStream
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({
"rack.version" => [0,1],
"rack.input" => cgi.stdinput,
"rack.errors" => $stderr,
"rack.multithread" => false,
"rack.multiprocess" => true,
"rack.run_once" => false,
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
})
env["QUERY_STRING"] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
status, headers, body = app.call(env)
begin
output.write(@cgi.header(@headers))
out.binmode if out.respond_to?(:binmode)
out.sync = false if out.respond_to?(:sync=)
if @cgi.__send__(:env_table)['REQUEST_METHOD'] == 'HEAD'
return
elsif @body.respond_to?(:call)
# Flush the output now in case the @body Proc uses
# #syswrite.
output.flush if output.respond_to?(:flush)
@body.call(self, output)
else
output.write(@body)
headers['Status'] = status.to_s
if headers.include?('Set-Cookie')
headers['cookie'] = headers.delete('Set-Cookie').split("\n")
end
output.flush if output.respond_to?(:flush)
rescue Errno::EPIPE, Errno::ECONNRESET
# lost connection to parent process, ignore output
out.write(cgi.header(headers))
body.each { |part|
out.write part
out.flush if out.respond_to?(:flush)
}
ensure
body.close if body.respond_to?(:close)
end
end
end
class CgiRequest #:nodoc:
DEFAULT_SESSION_OPTIONS = {
:database_manager => nil,
:prefix => "ruby_sess.",
:session_path => "/",
:session_key => "_session_id",
:cookie_only => true,
:session_http_only => true
}
end
end

View file

@ -1,169 +0,0 @@
module ActionController #:nodoc:
# Components allow you to call other actions for their rendered response while executing another action. You can either delegate
# the entire response rendering or you can mix a partial response in with your other content.
#
# class WeblogController < ActionController::Base
# # Performs a method and then lets hello_world output its render
# def delegate_action
# do_other_stuff_before_hello_world
# render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
# end
# end
#
# class GreeterController < ActionController::Base
# def hello_world
# render :text => "#{params[:person]} says, Hello World!"
# end
# end
#
# The same can be done in a view to do a partial rendering:
#
# 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
# 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.class_eval do
include InstanceMethods
include ActiveSupport::Deprecation
extend ClassMethods
helper HelperMethods
# 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
end
end
module ClassMethods
# Track parent controller to identify component requests
def process_with_components(request, response, parent_controller = nil) #:nodoc:
controller = new
controller.parent_controller = parent_controller
controller.process(request, response)
end
end
module HelperMethods
def render_component(options)
@controller.__send__(:render_component_as_string, options)
end
end
module InstanceMethods
# Extracts the action_name from the request parameters and performs that action.
def process_with_components(request, response, method = :perform_action, *arguments) #: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_for_text(component_response(options, true).body, response.headers["Status"])
end
end
deprecate :render_component => "Please install render_component plugin from http://github.com/rails/render_component/tree/master"
# Returns the component response as a string
def render_component_as_string(options) #:doc:
component_logging(options) do
response = component_response(options, false)
if redirected = response.redirected_to
render_component_as_string(redirected)
else
response.body
end
end
end
deprecate :render_component_as_string => "Please install render_component plugin from http://github.com/rails/render_component/tree/master"
def flash_with_components(refresh = false) #:nodoc:
if !defined?(@_flash) || refresh
@_flash =
if defined?(@parent_controller)
@parent_controller.flash
else
flash_without_components
end
end
@_flash
end
private
def component_response(options, reuse_response)
klass = component_class(options)
request = request_for_component(klass.controller_name, options)
new_response = reuse_response ? response : response.dup
klass.process_with_components(request, new_response, self)
end
# determine the controller class for the component request
def component_class(options)
if controller = options[:controller]
controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
else
self.class
end
end
# Create a new request object based on the current request.
# The new request inherits the session from the current request,
# bypassing any session options set for the component controller's class
def request_for_component(controller_name, options)
new_request = request.dup
new_request.session = request.session
new_request.instance_variable_set(
:@parameters,
(options[:params] || {}).with_indifferent_access.update(
"controller" => controller_name, "action" => options[:action], "id" => options[:id]
)
)
new_request
end
def component_logging(options)
if logger
logger.info "Start rendering component (#{options.inspect}): "
result = yield
logger.info "\n\nEnd of component rendering"
result
else
yield
end
end
def set_session_options_with_components(request)
set_session_options_without_components(request) unless component_request?
end
def process_cleanup_with_components
process_cleanup_without_components unless component_request?
end
end
end
end

View file

@ -41,7 +41,7 @@ module ActionController #:nodoc:
# * <tt>:expires</tt> - The time at which this cookie expires, as a Time object.
# * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
# Default is +false+.
# * <tt>:http_only</tt> - Whether this cookie is accessible via scripting or
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
# only HTTP. Defaults to +false+.
module Cookies
def self.included(base)
@ -51,7 +51,7 @@ module ActionController #:nodoc:
protected
# Returns the cookie container, which operates as described above.
def cookies
CookieJar.new(self)
@cookies ||= CookieJar.new(self)
end
end
@ -64,43 +64,32 @@ module ActionController #:nodoc:
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
def [](name)
cookie = @cookies[name.to_s]
if cookie && cookie.respond_to?(:value)
cookie.size > 1 ? cookie.value : cookie.value[0]
end
super(name.to_s)
end
# Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above.
def []=(name, options)
def []=(key, options)
if options.is_a?(Hash)
options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
options["name"] = name.to_s
options.symbolize_keys!
else
options = { "name" => name.to_s, "value" => options }
options = { :value => options }
end
set_cookie(options)
options[:path] = "/" unless options.has_key?(:path)
super(key.to_s, options[:value])
@controller.response.set_cookie(key, options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
def delete(name, options = {})
options.stringify_keys!
set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0)))
def delete(key, options = {})
options.symbolize_keys!
options[:path] = "/" unless options.has_key?(:path)
value = super(key.to_s)
@controller.response.delete_cookie(key, options)
value
end
private
# Builds a CGI::Cookie object and adds the cookie to the response headers.
#
# The path of the cookie defaults to "/" if there's none in +options+, and
# everything is passed to the CGI::Cookie constructor.
def set_cookie(options) #:doc:
options["path"] = "/" unless options["path"]
cookie = CGI::Cookie.new(options)
@controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil?
@controller.response.headers["cookie"] << cookie
end
end
end

View file

@ -2,27 +2,16 @@ 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
@@guard = Mutex.new
@@cache_classes = true
class << self
def define_dispatcher_callbacks(cache_classes)
@@cache_classes = cache_classes
unless cache_classes
# Development mode callbacks
before_dispatch :reload_application
after_dispatch :cleanup_application
end
# Common callbacks
to_prepare :load_application_controller do
begin
require_dependency 'application' unless defined?(::ApplicationController)
rescue LoadError => error
raise unless error.message =~ /application\.rb/
end
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
end
if defined?(ActiveRecord)
after_dispatch :checkin_connections
to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
end
@ -33,8 +22,7 @@ module ActionController
end
end
# Backward-compatible class method takes CGI-specific args. Deprecated
# in favor of Dispatcher.new(output, request, response).dispatch.
# DEPRECATE: Remove CGI support
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
new(output).dispatch_cgi(cgi, session_options)
end
@ -53,144 +41,93 @@ module ActionController
@prepare_dispatch_callbacks.replace_or_append!(callback)
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
def run_prepare_callbacks
if defined?(Rails) && Rails.logger
logger = Rails.logger
else
logger = Logger.new($stderr)
end
new(logger).send :run_callbacks, :prepare_dispatch
end
private
def failsafe_response_body(status)
error_path = "#{error_file_path}/#{status.to_s[0..3]}.html"
def reload_application
# Run prepare callbacks before every request in development mode
run_prepare_callbacks
if File.exist?(error_path)
File.read(error_path)
else
"<html><body><h1>#{status}</h1></body></html>"
end
end
Routing::Routes.reload
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
def cleanup_application
# Cleanup the application before processing the current request.
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end
end
cattr_accessor :error_file_path
self.error_file_path = Rails.public_path if defined?(Rails.public_path)
cattr_accessor :middleware
self.middleware = MiddlewareStack.new do |middleware|
middlewares = File.join(File.dirname(__FILE__), "middlewares.rb")
middleware.instance_eval(File.read(middlewares))
end
include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
# DEPRECATE: Remove arguments, since they are only used by CGI
def initialize(output = $stdout, request = nil, response = nil)
@output, @request, @response = output, request, response
@output = output
build_middleware_stack if @@cache_classes
end
def dispatch_unlocked
def dispatch
begin
run_callbacks :before_dispatch
handle_request
Routing::Routes.call(@env)
rescue Exception => exception
failsafe_rescue exception
if controller ||= (::ApplicationController rescue Base)
controller.call_with_exception(@env, exception).to_a
else
raise exception
end
ensure
run_callbacks :after_dispatch, :enumerator => :reverse_each
end
end
def dispatch
if ActionController::Base.allow_concurrency
dispatch_unlocked
# DEPRECATE: Remove CGI support
def dispatch_cgi(cgi, session_options)
CGIHandler.dispatch_cgi(self, cgi, @output)
end
def call(env)
if @@cache_classes
@app.call(env)
else
@@guard.synchronize do
dispatch_unlocked
Reloader.run do
# When class reloading is turned on, we will want to rebuild the
# middleware stack every time we process a request. If we don't
# rebuild the middleware stack, then the stack may contain references
# to old classes metal classes, which will b0rk class reloading.
build_middleware_stack
@app.call(env)
end
end
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 call(env)
@request = RackRequest.new(env)
@response = RackResponse.new(@request)
def _call(env)
@env = env
dispatch
end
def reload_application
# Run prepare callbacks before every request in development mode
run_callbacks :prepare_dispatch
Routing::Routes.reload
ActionController::Base.view_paths.reload!
ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear
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
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end
def flush_logger
Base.logger.flush
end
def mark_as_test_request!
@test_request = true
self
end
def test_request?
@test_request
end
def checkin_connections
# Don't return connection (and peform implicit rollback) if this request is a part of integration test
return if test_request?
ActiveRecord::Base.clear_active_connections!
end
protected
def handle_request
@controller = Routing::Routes.recognize(@request)
@controller.process(@request, @response).out(@output)
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
private
def build_middleware_stack
@app = @@middleware.build(lambda { |env| self.dup._call(env) })
end
end
end

View file

@ -0,0 +1,86 @@
require 'erb'
module ActionController
# The Failsafe middleware is usually the top-most middleware in the Rack
# middleware chain. It returns the underlying middleware's response, but if
# the underlying middle raises an exception then Failsafe will log the
# exception into the Rails log file, and will attempt to return an error
# message response.
#
# Failsafe is a last resort for logging errors and for telling the HTTP
# client that something went wrong. Do not confuse this with the
# ActionController::Rescue module, which is responsible for catching
# exceptions at deeper levels. Unlike Failsafe, which is as simple as
# possible, Rescue provides features that allow developers to hook into
# the error handling logic, and can customize the error message response
# based on the HTTP client's IP.
class Failsafe
cattr_accessor :error_file_path
self.error_file_path = Rails.public_path if defined?(Rails.public_path)
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
rescue Exception => exception
# Reraise exception in test environment
if defined?(Rails) && Rails.env.test?
raise exception
else
failsafe_response(exception)
end
end
private
def failsafe_response(exception)
log_failsafe_exception(exception)
[500, {'Content-Type' => 'text/html'}, [failsafe_response_body]]
rescue Exception => failsafe_error # Logger or IO errors
$stderr.puts "Error during failsafe response: #{failsafe_error}"
end
def failsafe_response_body
error_template_path = "#{self.class.error_file_path}/500.html"
if File.exist?(error_template_path)
begin
result = render_template(error_template_path)
rescue Exception
result = nil
end
else
result = nil
end
if result.nil?
result = "<html><body><h1>500 Internal Server Error</h1>" <<
"If you are the administrator of this website, then please read this web " <<
"application's log file to find out what went wrong.</body></html>"
end
result
end
# The default 500.html uses the h() method.
def h(text) # :nodoc:
ERB::Util.h(text)
end
def render_template(filename)
ERB.new(File.read(filename)).result(binding)
end
def log_failsafe_exception(exception)
message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: 500 Internal Server Error\n"
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
failsafe_logger.fatal(message)
end
def failsafe_logger
if defined?(Rails) && Rails.logger
Rails.logger
else
Logger.new($stderr)
end
end
end
end

View file

@ -4,20 +4,22 @@ module ActionController #:nodoc:
# 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
# class PostsController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Successfully created post"
# redirect_to :action => "display", :params => { :id => post.id }
# redirect_to posts_path(@post)
# end
#
# def display
# def show
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# display.erb
# <% if flash[:notice] %><div class="notice"><%= flash[:notice] %></div><% end %>
# show.html.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.
@ -27,55 +29,54 @@ module ActionController #:nodoc:
def self.included(base)
base.class_eval do
include InstanceMethods
alias_method_chain :assign_shortcuts, :flash
alias_method_chain :reset_session, :flash
alias_method_chain :perform_action, :flash
alias_method_chain :reset_session, :flash
end
end
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
end
def []=(k, v)
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
@flash[k]
end
end
class FlashHash < Hash
def initialize #:nodoc:
super
@used = {}
end
def []=(k, v) #:nodoc:
keep(k)
super
end
def update(h) #:nodoc:
h.keys.each { |k| keep(k) }
super
end
alias :merge! :update
def replace(h) #:nodoc:
@used = {}
super
end
# Sets a flash that will not be available to the next action, only to the current.
#
# flash.now[:message] = "Hello current action"
#
#
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
@ -85,7 +86,7 @@ module ActionController #:nodoc:
def now
FlashNow.new(self)
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
#
# flash.keep # keeps the entire flash
@ -93,7 +94,7 @@ module ActionController #:nodoc:
def keep(k = nil)
use(k, false)
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
#
# flash.discard # discard the entire flash at the end of the current action
@ -101,12 +102,12 @@ module ActionController #:nodoc:
def discard(k = nil)
use(k)
end
# Mark for removal entries that were kept, and delete unkept ones.
#
# This method is called automatically by filters, so you generally don't need to care about it.
def sweep #:nodoc:
keys.each do |k|
keys.each do |k|
unless @used[k]
use(k)
else
@ -118,7 +119,12 @@ module ActionController #:nodoc:
# 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
def store(session, key = "flash")
return if self.empty?
session[key] = self
end
private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
@ -136,37 +142,30 @@ module ActionController #:nodoc:
module InstanceMethods #:nodoc:
protected
def perform_action_with_flash
perform_action_without_flash
if defined? @_flash
@_flash.store(session)
remove_instance_variable(:@_flash)
end
end
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash)
flash(:refresh)
remove_instance_variable(:@_flash) if defined? @_flash
end
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
# <tt>flash["notice"] = "hello"</tt> to put a new one.
# Note that if sessions are disabled only flash.now will work.
def flash(refresh = false) #:doc:
if !defined?(@_flash) || refresh
@_flash =
if session.is_a?(Hash)
# don't put flash in session if disabled
FlashHash.new
else
# otherwise, session is a CGI::Session or a TestSession
# so make sure it gets retrieved from/saved to session storage after request processing
session["flash"] ||= FlashHash.new
end
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash #:doc:
if !defined?(@_flash)
@_flash = session["flash"] || FlashHash.new
@_flash.sweep
end
@_flash
end
private
def assign_shortcuts_with_flash(request, response) #:nodoc:
assign_shortcuts_without_flash(request, response)
flash(:refresh)
flash.sweep if @_session && !component_request?
end
end
end
end

View file

@ -1,13 +1,17 @@
require 'active_support/dependencies'
# 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
base.master_helper_module = Module.new
# Set the default directory for helpers
base.class_inheritable_accessor :helpers_dir
base.helpers_dir = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers")
# Extend base with class methods to declare helpers.
base.extend(ClassMethods)
@ -88,8 +92,8 @@ module ActionController #:nodoc:
# 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.
# When the argument is the symbol <tt>:all</tt>, the controller will include all helpers beneath
# <tt>ActionController::Base.helpers_dir</tt> (defaults to <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT).
# helper :all
#
# Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available
@ -159,9 +163,9 @@ module ActionController #:nodoc:
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
def #{method}(*args, &block)
controller.send(%(#{method}), *args, &block)
end
def #{method}(*args, &block) # def current_user(*args, &block)
controller.send(%(#{method}), *args, &block) # controller.send(%(current_user), *args, &block)
end # end
end_eval
end
end
@ -213,8 +217,8 @@ module ActionController #:nodoc:
# 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' }
extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/
Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
end
end
end

View file

@ -1,42 +1,42 @@
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|
# 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,
#
#
# 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
@ -54,24 +54,55 @@ module ActionController
# end
# end
# end
#
#
#
# In your integration tests, you can do something like this:
#
#
# def test_access_granted_from_xml
# get(
# "/notes/1.xml", nil,
# "/notes/1.xml", nil,
# :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
# )
#
#
# assert_equal 200, status
# end
#
#
#
# Simple Digest example:
#
# require 'digest/md5'
# class PostsController < ApplicationController
# REALM = "SuperSecret"
# USERS = {"dhh" => "secret", #plain text password
# "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password
#
# before_filter :authenticate, :except => [:index]
#
# def index
# render :text => "Everyone can see me!"
# end
#
# def edit
# render :text => "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_digest(REALM) do |username|
# USERS[username]
# end
# end
# end
#
# NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately
# hash to check the user's credentials. Returning +nil+ will cause authentication to fail.
# Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
# the password file or database is compromised, the attacker would be able to use the ha1 hash to
# authenticate as the user at this +realm+, but would not have the user's password to try using at
# other sites.
#
# 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 your Apache setup:
#
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Basic
extend self
@ -99,16 +130,16 @@ module ActionController
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)
ActiveSupport::Base64.decode64(authorization(request).split.last || '')
ActiveSupport::Base64.decode64(authorization(request).split(' ', 2).last || '')
end
def encode_credentials(user_name, password)
@ -120,5 +151,159 @@ module ActionController
controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end
end
module Digest
extend self
module ControllerMethods
def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm)
end
# Authenticate with HTTP Digest, returns true or false
def authenticate_with_http_digest(realm = "Application", &password_procedure)
HttpAuthentication::Digest.authenticate(self, realm, &password_procedure)
end
# Render output including the HTTP Digest authentication header
def request_http_digest_authentication(realm = "Application", message = nil)
HttpAuthentication::Digest.authentication_request(self, realm, message)
end
end
# Returns false on a valid response, true otherwise
def authenticate(controller, realm, &password_procedure)
authorization(controller.request) && validate_digest_response(controller.request, realm, &password_procedure)
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
# Returns false unless the request credentials response value matches the expected value.
# First try the password as a ha1 digest password. If this fails, then try it as a plain
# text password.
def validate_digest_response(request, realm, &password_procedure)
credentials = decode_credentials_header(request)
valid_nonce = validate_nonce(request, credentials[:nonce])
if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
password = password_procedure.call(credentials[:username])
return false unless password
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
uri = credentials[:uri][0,1] == '/' ? request.request_uri : request.url
[true, false].any? do |password_is_ha1|
expected = expected_response(method, uri, credentials, password, password_is_ha1)
expected == credentials[:response]
end
end
end
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
# of a plain-text password.
def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(':'))
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(':'))
end
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
end
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
"Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
end
def decode_credentials_header(request)
decode_credentials(authorization(request))
end
def decode_credentials(header)
header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}.with_indifferent_access) do |hash, pair|
key, value = pair.split('=', 2)
hash[key.strip] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
hash
end
end
def authentication_header(controller, realm)
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Digest: Access denied.\n"
authentication_header(controller, realm)
controller.__send__ :render, :text => message, :status => :unauthorized
end
# Uses an MD5 digest based on time to generate a value to be used only once.
#
# A server-specified data string which should be uniquely generated each time a 401 response is made.
# It is recommended that this string be base64 or hexadecimal data.
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
#
# The contents of the nonce are implementation dependent.
# The quality of the implementation depends on a good choice.
# A nonce might, for example, be constructed as the base 64 encoding of
#
# => time-stamp H(time-stamp ":" ETag ":" private-key)
#
# where time-stamp is a server-generated time or other non-repeating value,
# ETag is the value of the HTTP ETag header associated with the requested entity,
# and private-key is data known only to the server.
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
# reject the request if it did not match the nonce from that header or
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
# to limit the reuse of the nonce to the same client that originally got it.
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
# Also, IP address spoofing is not that hard.)
#
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
# POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# of this document.
#
# The nonce is opaque to the client. Composed of Time, and hash of Time with secret
# key from the Rails session secret generated upon creation of project. Ensures
# the time cannot be modifed by client.
def nonce(time = Time.now)
t = time.to_i
hashed = [t, secret_key]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
Base64.encode64("#{t}:#{digest}").gsub("\n", '')
end
# Might want a shorter timeout depending on whether the request
# is a PUT or POST, and if client is browser or web service.
# Can be much shorter if the Stale directive is implemented. This would
# allow a user to use new nonce without prompting user again for their
# username and password.
def validate_nonce(request, value, seconds_to_timeout=5*60)
return false if value.nil?
t = Base64.decode64(value).split(":").first.to_i
nonce(t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end
# Opaque based on random generation - but changing each request?
def opaque()
::Digest::MD5.hexdigest(secret_key)
end
# Set in /initializers/session_store.rb, and loaded even if sessions are not in use.
def secret_key
ActionController::Base.session_options[:secret]
end
end
end
end

View file

@ -1,30 +1,35 @@
require 'active_support/test_case'
require 'action_controller/dispatcher'
require 'action_controller/test_process'
require 'stringio'
require 'uri'
require 'active_support/test_case'
require 'action_controller/rack_lint_patch'
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
# performed sequentially by some virtual user. Becase you can instantiate
# performed sequentially by some virtual user. Because you can instantiate
# multiple sessions and run them side-by-side, you can also mimic (to some
# limited extent) multiple simultaneous users interacting with your system.
#
# Typically, you will instantiate a new session using IntegrationTest#open_session,
# rather than instantiating Integration::Session directly.
# Typically, you will instantiate a new session using
# IntegrationTest#open_session, rather than instantiating
# Integration::Session directly.
class Session
include Test::Unit::Assertions
include ActionController::Assertions
include ActionController::TestCase::Assertions
include ActionController::TestProcess
# Rack application to use
attr_accessor :application
# The integer HTTP status code of the last request.
attr_reader :status
# The status message that accompanied the status code of the last request.
attr_reader :status_message
# The body of the last request.
attr_reader :body
# The URI of the last request.
attr_reader :path
@ -60,7 +65,8 @@ module ActionController
end
# Create and initialize a new Session instance.
def initialize
def initialize(app = nil)
@application = app || ActionController::Dispatcher.new
reset!
end
@ -79,11 +85,13 @@ module ActionController
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"
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 defined? @named_routes_configured
# install the named routes in this session instance.
klass = class<<self; self; end
klass = class << self; self; end
Routing::Routes.install_helpers(klass)
# the helpers are made protected by default--we make them public for
@ -97,7 +105,7 @@ module ActionController
#
# session.https!
# session.https!(false)
def https!(flag=true)
def https!(flag = true)
@https = flag
end
@ -122,7 +130,7 @@ module ActionController
# performed on the location header.
def follow_redirect!
raise "not a redirect! #{@status} #{@status_message}" unless redirect?
get(interpret_uri(headers['location'].first))
get(interpret_uri(headers['location']))
status
end
@ -167,17 +175,21 @@ module ActionController
# Performs a GET request with the given parameters.
#
# - +path+: The URI (as a String) on which you want to perform a GET request.
# - +parameters+: The HTTP parameters that you want to pass. This may be +nil+,
# - +path+: The URI (as a String) on which you want to perform a GET
# request.
# - +parameters+: The HTTP parameters that you want to pass. This may
# be +nil+,
# a Hash, or a String that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
# (<tt>application/x-www-form-urlencoded</tt> or
# <tt>multipart/form-data</tt>).
# - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
# automatically be upcased, with the prefix 'HTTP_' added if needed.
#
# This method returns an AbstractResponse object, which one can use to inspect
# the details of the response. Furthermore, if this method was called from an
# ActionController::IntegrationTest object, then that object's <tt>@response</tt>
# instance variable will point to the same response object.
# This method returns an Response object, which one can use to
# inspect the details of the response. Furthermore, if this method was
# called from an ActionController::IntegrationTest object, then that
# object's <tt>@response</tt> instance variable will point to the same
# response object.
#
# You can also perform POST, PUT, DELETE, and HEAD requests with +post+,
# +put+, +delete+, and +head+.
@ -185,22 +197,26 @@ module ActionController
process :get, path, parameters, headers
end
# Performs a POST request with the given parameters. See get() for more details.
# Performs a POST request with the given parameters. See get() for more
# details.
def post(path, parameters = nil, headers = nil)
process :post, path, parameters, headers
end
# Performs a PUT request with the given parameters. See get() for more details.
# Performs a PUT request with the given parameters. See get() for more
# details.
def put(path, parameters = nil, headers = nil)
process :put, path, parameters, headers
end
# Performs a DELETE request with the given parameters. See get() for more details.
# Performs a DELETE request with the given parameters. See get() for
# more details.
def delete(path, parameters = nil, headers = nil)
process :delete, path, parameters, headers
end
# Performs a HEAD request with the given parameters. See get() for more details.
# Performs a HEAD request with the given parameters. See get() for more
# details.
def head(path, parameters = nil, headers = nil)
process :head, path, parameters, headers
end
@ -215,8 +231,7 @@ module ActionController
def xml_http_request(request_method, path, parameters = nil, headers = nil)
headers ||= {}
headers['X-Requested-With'] = 'XMLHttpRequest'
headers['Accept'] ||= 'text/javascript, text/html, application/xml, text/xml, */*'
headers['Accept'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
process(request_method, path, parameters, headers)
end
alias xhr :xml_http_request
@ -224,7 +239,9 @@ module ActionController
# Returns the URL for the given options, according to the rules specified
# in the application's routes.
def url_for(options)
controller ? controller.url_for(options) : generic_url_rewriter.rewrite(options)
controller ?
controller.url_for(options) :
generic_url_rewriter.rewrite(options)
end
private
@ -250,17 +267,35 @@ module ActionController
data = nil
end
env["QUERY_STRING"] ||= ""
data ||= ''
data.force_encoding(Encoding::ASCII_8BIT) if data.respond_to?(:force_encoding)
data = data.is_a?(IO) ? data : StringIO.new(data)
env.update(
"REQUEST_METHOD" => method.to_s.upcase,
"REQUEST_METHOD" => method.to_s.upcase,
"SERVER_NAME" => host,
"SERVER_PORT" => (https? ? "443" : "80"),
"HTTPS" => https? ? "on" : "off",
"rack.url_scheme" => https? ? "https" : "http",
"SCRIPT_NAME" => "",
"REQUEST_URI" => path,
"PATH_INFO" => path,
"HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr,
"SERVER_PORT" => (https? ? "443" : "80"),
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
"CONTENT_LENGTH" => data ? data.length.to_s : nil,
"HTTP_COOKIE" => encode_cookies,
"HTTPS" => https? ? "on" : "off",
"HTTP_ACCEPT" => accept
"HTTP_ACCEPT" => accept,
"rack.version" => [0,1],
"rack.input" => data,
"rack.errors" => StringIO.new,
"rack.multithread" => true,
"rack.multiprocess" => true,
"rack.run_once" => false
)
(headers || {}).each do |key, value|
@ -269,54 +304,62 @@ module ActionController
env[key] = value
end
unless ActionController::Base.respond_to?(:clear_last_instantiation!)
ActionController::Base.module_eval { include ControllerCapture }
[ControllerCapture, ActionController::ProcessWithTest].each do |mod|
unless ActionController::Base < mod
ActionController::Base.class_eval { include mod }
end
end
ActionController::Base.clear_last_instantiation!
env['rack.input'] = data.is_a?(IO) ? data : StringIO.new(data || '')
@status, @headers, result_body = ActionController::Dispatcher.new.mark_as_test_request!.call(env)
app = Rack::Lint.new(@application)
status, headers, body = app.call(env)
@request_count += 1
@controller = ActionController::Base.last_instantiation
@request = @controller.request
@response = @controller.response
# Decorate the response with the standard behavior of the TestResponse
# so that things like assert_response can be used in integration
# tests.
@response.extend(TestResponseBehavior)
@html_document = nil
# Inject status back in for backwords compatibility with CGI
@headers['Status'] = @status
@status = status.to_i
@status_message = StatusCodes::STATUS_CODES[@status]
@status, @status_message = @status.split(/ /)
@status = @status.to_i
@headers = Rack::Utils::HeaderHash.new(headers)
cgi_headers = Hash.new { |h,k| h[k] = [] }
@headers.each do |key, value|
cgi_headers[key.downcase] << value
end
cgi_headers['set-cookie'] = cgi_headers['set-cookie'].first
@headers = cgi_headers
@response.headers['cookie'] ||= []
(@headers['set-cookie'] || []).each do |cookie|
(@headers['Set-Cookie'] || "").split("\n").each do |cookie|
name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2]
@cookies[name] = value
# Fake CGI cookie header
# DEPRECATE: Use response.headers["Set-Cookie"] instead
@response.headers['cookie'] << CGI::Cookie::new("name" => name, "value" => value)
end
return status
@body = ""
if body.respond_to?(:to_str)
@body << body
else
body.each { |part| @body << part }
end
if @controller = ActionController::Base.last_instantiation
@request = @controller.request
@response = @controller.response
@controller.send(:set_test_assigns)
else
# Decorate responses from Rack Middleware and Rails Metal
# as an Response for the purposes of integration testing
@response = Response.new
@response.status = status.to_s
@response.headers.replace(@headers)
@response.body = @body
end
# Decorate the response with the standard behavior of the
# TestResponse so that things like assert_response can be
# used in integration tests.
@response.extend(TestResponseBehavior)
return @status
rescue MultiPartNeededException
boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
status = process(method, path, multipart_body(parameters, boundary), (headers || {}).merge({"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}"}))
status = process(method, path,
multipart_body(parameters, boundary),
(headers || {}).merge(
{"CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}"}))
return status
end
@ -338,7 +381,7 @@ module ActionController
"SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off"
}
ActionController::UrlRewriter.new(ActionController::RackRequest.new(env), {})
UrlRewriter.new(Request.new(env), {})
end
def name_with_prefix(prefix, name)
@ -352,9 +395,13 @@ module ActionController
raise MultiPartNeededException
elsif Hash === parameters
return nil if parameters.empty?
parameters.map { |k,v| requestify(v, name_with_prefix(prefix, k)) }.join("&")
parameters.map { |k,v|
requestify(v, name_with_prefix(prefix, k))
}.join("&")
elsif Array === parameters
parameters.map { |v| requestify(v, name_with_prefix(prefix, "")) }.join("&")
parameters.map { |v|
requestify(v, name_with_prefix(prefix, ""))
}.join("&")
elsif prefix.nil?
parameters
else
@ -365,7 +412,7 @@ module ActionController
def multipart_requestify(params, first=true)
returning Hash.new do |p|
params.each do |key, value|
k = first ? CGI.escape(key.to_s) : "[#{CGI.escape(key.to_s)}]"
k = first ? key.to_s : "[#{key.to_s}]"
if Hash === value
multipart_requestify(value, false).each do |subkey, subvalue|
p[k + subkey] = subvalue
@ -380,7 +427,7 @@ module ActionController
def multipart_body(params, boundary)
multipart_requestify(params).map do |key, value|
if value.respond_to?(:original_filename)
File.open(value.path) do |f|
File.open(value.path, "rb") do |f|
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
<<-EOF
@ -432,6 +479,11 @@ EOF
end
module Runner
def initialize(*args)
super
@integration_session = nil
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
@ -460,8 +512,8 @@ EOF
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
def open_session
session = Integration::Session.new
def open_session(application = nil)
session = Integration::Session.new(application)
# delegate the fixture accessors back to the test instance
extras = Module.new { attr_accessor :delegate, :test_result }
@ -469,12 +521,16 @@ EOF
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) }
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 }
extras.__send__(:define_method, :add_assertion) {
test_result.add_assertion
}
session.extend(extras)
session.delegate = self
session.test_result = @_result
@ -495,8 +551,12 @@ EOF
# 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!
if @integration_session.respond_to?(sym)
returning @integration_session.__send__(sym, *args, &block) do
copy_session_variables!
end
else
super
end
end
end
@ -602,7 +662,8 @@ EOF
# would potentially have to set their values for both Test::Unit::TestCase
# ActionController::IntegrationTest, since by the time the value is set on
# TestCase, IntegrationTest has already been defined and cannot inherit
# changes to those variables. So, we make those two attributes copy-on-write.
# changes to those variables. So, we make those two attributes
# copy-on-write.
class << self
def use_transactional_fixtures=(flag) #:nodoc:

View file

@ -172,16 +172,8 @@ module ActionController #:nodoc:
@layout_conditions ||= read_inheritable_attribute(:layout_conditions)
end
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:
Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] }
Array(view_paths).sum([]) { |path| Dir["#{path.to_str}/layouts/**/*"] }
end
private
@ -200,45 +192,43 @@ module ActionController #:nodoc:
def normalize_conditions(conditions)
conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
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
def initialize(*args)
super
@real_format = nil
end
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
# 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(default_template_format)
def active_layout(passed_layout = nil, options = {})
layout = passed_layout || default_layout
return layout if layout.respond_to?(:render)
active_layout = case layout
when String then layout
when Symbol then __send__(layout)
when Proc then layout.call(self)
else layout
end
# Explicitly passed layout names with slashes are looked up relative to the template root,
# but auto-discovered layouts derived from a nested controller will contain a slash, though be relative
# to the 'layouts' directory so we have to check the file system to infer which case the layout name came from.
if active_layout
if active_layout.include?('/') && ! layout_directory?(active_layout)
active_layout
else
"layouts/#{active_layout}"
end
end
find_layout(active_layout, default_template_format, options[:html_fallback]) if active_layout
end
private
def candidate_for_layout?(options)
options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? &&
!@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action]))
def default_layout #:nodoc:
layout = self.class.read_inheritable_attribute(:layout)
return layout unless self.class.read_inheritable_attribute(:auto_layout)
find_layout(layout, default_template_format)
rescue ActionView::MissingTemplate
nil
end
def find_layout(layout, format, html_fallback=false) #:nodoc:
view_paths.find_template(layout.to_s =~ /\A\/|layouts\// ? layout : "layouts/#{layout}", format, html_fallback)
rescue ActionView::MissingTemplate
raise if Mime::Type.lookup_by_extension(format.to_s).html?
end
def pick_layout(options)
@ -247,9 +237,9 @@ module ActionController #:nodoc:
when FalseClass
nil
when NilClass, TrueClass
active_layout if action_has_layout? && !@template.__send__(:_exempt_from_layout?, default_template_name)
active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
active_layout(layout)
active_layout(layout, :html_fallback => true)
end
else
active_layout if action_has_layout? && candidate_for_layout?(options)
@ -271,14 +261,26 @@ module ActionController #:nodoc:
end
end
def layout_directory?(layout_name)
@template.__send__(:_pick_template, "#{File.join('layouts', layout_name)}.#{@template.template_format}") ? true : false
def candidate_for_layout?(options)
template = options[:template] || default_template(options[:action])
if options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty?
begin
template_object = self.view_paths.find_template(template, default_template_format)
# this restores the behavior from 2.2.2, where response.template.template_format was reset
# to :html for :js requests with a matching html template.
# see v2.2.2, ActionView::Base, lines 328-330
@real_format = :html if response.template.template_format == :js && template_object.format == "html"
!template_object.exempt_from_layout?
rescue ActionView::MissingTemplate
true
end
end
rescue ActionView::MissingTemplate
false
end
def default_template_format
response.template.template_format
@real_format || response.template.template_format
end
end
end

View file

@ -0,0 +1,119 @@
module ActionController
class MiddlewareStack < Array
class Middleware
def self.new(klass, *args, &block)
if klass.is_a?(self)
klass
else
super
end
end
attr_reader :args, :block
def initialize(klass, *args, &block)
@klass = klass
options = args.extract_options!
if options.has_key?(:if)
@conditional = options.delete(:if)
else
@conditional = true
end
args << options unless options.empty?
@args = args
@block = block
end
def klass
if @klass.respond_to?(:call)
@klass.call
elsif @klass.is_a?(Class)
@klass
else
@klass.to_s.constantize
end
rescue NameError
@klass
end
def active?
return false unless klass
if @conditional.respond_to?(:call)
@conditional.call
else
@conditional
end
end
def ==(middleware)
case middleware
when Middleware
klass == middleware.klass
when Class
klass == middleware
else
klass == middleware.to_s.constantize
end
end
def inspect
str = klass.to_s
args.each { |arg| str += ", #{arg.inspect}" }
str
end
def build(app)
if block
klass.new(app, *build_args, &block)
else
klass.new(app, *build_args)
end
end
private
def build_args
Array(args).map { |arg| arg.respond_to?(:call) ? arg.call : arg }
end
end
def initialize(*args, &block)
super(*args)
block.call(self) if block_given?
end
def insert(index, *args, &block)
index = self.index(index) unless index.is_a?(Integer)
middleware = Middleware.new(*args, &block)
super(index, middleware)
end
alias_method :insert_before, :insert
def insert_after(index, *args, &block)
index = self.index(index) unless index.is_a?(Integer)
insert(index + 1, *args, &block)
end
def swap(target, *args, &block)
insert_before(target, *args, &block)
delete(target)
end
def use(*args, &block)
middleware = Middleware.new(*args, &block)
push(middleware)
end
def active
find_all { |middleware| middleware.active? }
end
def build(app)
active.reverse.inject(app) { |a, e| e.build(a) }
end
end
end

View file

@ -0,0 +1,14 @@
use "Rack::Lock", :if => lambda {
!ActionController::Base.allow_concurrency
}
use "ActionController::Failsafe"
use lambda { ActionController::Base.session_store },
lambda { ActionController::Base.session_options }
use "ActionController::ParamsParser"
use "Rack::MethodOverride"
use "Rack::Head"
use "ActionController::StringCoercion"

View file

@ -143,12 +143,27 @@ module ActionController #:nodoc:
custom(@mime_type_priority.first, &block)
end
end
def self.generate_method_for_mime(mime)
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
const = sym.to_s.upcase
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{sym}(&block) # def html(&block)
custom(Mime::#{const}, &block) # custom(Mime::HTML, &block)
end # end
RUBY
end
Mime::SET.each do |mime|
generate_method_for_mime(mime)
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)
mime_constant = Mime.const_get(symbol.to_s.upcase)
if Mime::SET.include?(mime_constant)
self.class.generate_method_for_mime(mime_constant)
send(symbol, &block)
else
super
end

View file

@ -176,6 +176,14 @@ module Mime
end
end
def =~(mime_type)
return false if mime_type.blank?
regexp = Regexp.new(Regexp.quote(mime_type.to_s))
(@synonyms + [ self ]).any? do |synonym|
synonym.to_s =~ regexp
end
end
# Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
# ActionController::RequestForgeryProtection.
def verify_request?

View file

@ -0,0 +1,77 @@
module ActionController
class ParamsParser
ActionController::Base.param_parsers[Mime::XML] = :xml_simple
ActionController::Base.param_parsers[Mime::JSON] = :json
def initialize(app)
@app = app
end
def call(env)
if params = parse_formatted_parameters(env)
env["action_controller.request.request_parameters"] = params
end
@app.call(env)
end
private
def parse_formatted_parameters(env)
request = Request.new(env)
return false if request.content_length.zero?
mime_type = content_type_from_legacy_post_data_format_header(env) || request.content_type
strategy = ActionController::Base.param_parsers[mime_type]
return false unless strategy
case strategy
when Proc
strategy.call(request.raw_post)
when :xml_simple, :xml_node
body = request.raw_post
body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
when :yaml
YAML.load(request.raw_post)
when :json
body = request.raw_post
if body.blank?
{}
else
data = ActiveSupport::JSON.decode(body)
data = {:_json => data} unless data.is_a?(Hash)
data.with_indifferent_access
end
else
false
end
rescue Exception => e # YAML, XML or Ruby code block errors
logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
raise
{ "body" => request.raw_post,
"content_type" => request.content_type,
"content_length" => request.content_length,
"exception" => "#{e.message} (#{e.class})",
"backtrace" => e.backtrace }
end
def content_type_from_legacy_post_data_format_header(env)
if x_post_format = env['HTTP_X_POST_DATA_FORMAT']
case x_post_format.to_s.downcase
when 'yaml'
return Mime::YAML
when 'xml'
return Mime::XML
end
end
nil
end
def logger
defined?(Rails.logger) ? Rails.logger : Logger.new($stderr)
end
end
end

View file

@ -1,4 +1,3 @@
require 'action_controller/integration'
require 'active_support/testing/performance'
require 'active_support/testing/default'

View file

@ -36,12 +36,11 @@ module ActionController
#
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
# * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
# * <tt>formatted_polymorphic_url</tt>, <tt>formatted_polymorphic_path</tt>
#
# Example usage:
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# formatted_polymorphic_path([@post, :pdf]) # => "/posts/1.pdf"
# polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
module PolymorphicRoutes
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
@ -55,7 +54,7 @@ module ActionController
# ==== Options
#
# * <tt>:action</tt> - Specifies the action prefix for the named route:
# <tt>:new</tt>, <tt>:edit</tt>, or <tt>:formatted</tt>. Default is no prefix.
# <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
@ -77,10 +76,8 @@ module ActionController
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
end
record = extract_record(record_or_hash_or_array)
format = extract_format(record_or_hash_or_array, options)
namespace = extract_namespace(record_or_hash_or_array)
record = extract_record(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
@ -100,11 +97,9 @@ module ActionController
end
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
args << format if format
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options)
named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
url_options = options.except(:action, :routing_type, :format)
url_options = options.except(:action, :routing_type)
unless url_options.empty?
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end
@ -119,28 +114,44 @@ module ActionController
polymorphic_url(record_or_hash_or_array, options)
end
%w(edit new formatted).each do |action|
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
def #{action}_polymorphic_url(record_or_hash, options = {})
polymorphic_url(record_or_hash, options.merge(:action => "#{action}"))
end
def #{action}_polymorphic_path(record_or_hash, options = {})
polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path))
end
def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
polymorphic_url( # polymorphic_url(
record_or_hash, # record_or_hash,
options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
end # end
#
def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
polymorphic_url( # polymorphic_url(
record_or_hash, # record_or_hash,
options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
end # end
EOT
end
def formatted_polymorphic_url(record_or_hash, options = {})
ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller)
options[:format] = record_or_hash.pop if Array === record_or_hash
polymorphic_url(record_or_hash, options)
end
def formatted_polymorphic_path(record_or_hash, options = {})
ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller)
options[:format] = record_or_hash.pop if record_or_hash === Array
polymorphic_url(record_or_hash, options.merge(:routing_type => :path))
end
private
def action_prefix(options)
options[:action] ? "#{options[:action]}_" : options[:format] ? "formatted_" : ""
options[:action] ? "#{options[:action]}_" : ''
end
def routing_type(options)
options[:routing_type] || :url
end
def build_named_route_call(records, namespace, inflection, options = {})
def build_named_route_call(records, inflection, options = {})
unless records.is_a?(Array)
record = extract_record(records)
route = ''
@ -150,7 +161,8 @@ module ActionController
if parent.is_a?(Symbol) || parent.is_a?(String)
string << "#{parent}_"
else
string << "#{RecordIdentifier.__send__("singular_class_name", parent)}_"
string << RecordIdentifier.__send__("plural_class_name", parent).singularize
string << "_"
end
end
end
@ -158,10 +170,12 @@ module ActionController
if record.is_a?(Symbol) || record.is_a?(String)
route << "#{record}_"
else
route << "#{RecordIdentifier.__send__("#{inflection}_class_name", record)}_"
route << RecordIdentifier.__send__("plural_class_name", record)
route = route.singularize if inflection == :singular
route << "_"
end
action_prefix(options) + namespace + route + routing_type(options).to_s
action_prefix(options) + route + routing_type(options).to_s
end
def extract_record(record_or_hash_or_array)
@ -171,28 +185,5 @@ module ActionController
else record_or_hash_or_array
end
end
def extract_format(record_or_hash_or_array, options)
if options[:action].to_s == "formatted" && record_or_hash_or_array.is_a?(Array)
record_or_hash_or_array.pop
elsif options[:format]
options[:format]
else
nil
end
end
# Remove the first symbols from the array and return the url prefix
# implied by those symbols.
def extract_namespace(record_or_hash_or_array)
return "" unless record_or_hash_or_array.is_a?(Array)
namespace_keys = []
while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol)
namespace_keys << record_or_hash_or_array.shift
end
namespace_keys.map {|k| "#{k}_"}.join
end
end
end

View file

@ -0,0 +1,36 @@
# Rack 1.0 does not allow string subclass body. This does not play well with our ActionView::SafeBuffer.
# The next release of Rack will be allowing string subclass body - http://github.com/rack/rack/commit/de668df02802a0335376a81ba709270e43ba9d55
# TODO : Remove this monkey patch after the next release of Rack
module RackLintPatch
module AllowStringSubclass
def self.included(base)
base.send :alias_method, :each, :each_with_hack
end
def each_with_hack
@closed = false
@body.each { |part|
assert("Body yielded non-string value #{part.inspect}") {
part.kind_of?(String)
}
yield part
}
if @body.respond_to?(:to_path)
assert("The file identified by body.to_path does not exist") {
::File.exist? @body.to_path
}
end
end
end
begin
app = proc {|env| [200, {"Content-Type" => "text/plain", "Content-Length" => "12"}, [Class.new(String).new("Hello World!")]] }
response = Rack::MockRequest.new(Rack::Lint.new(app)).get('/')
rescue Rack::Lint::LintError => e
raise(e) unless e.message =~ /Body yielded non-string value/
Rack::Lint.send :include, AllowStringSubclass
end
end

View file

@ -1,303 +0,0 @@
require 'action_controller/cgi_ext'
require 'action_controller/session/cookie_store'
module ActionController #:nodoc:
class RackRequest < AbstractRequest #:nodoc:
attr_accessor :session_options
attr_reader :cgi
class SessionFixationAttempt < StandardError #:nodoc:
end
DEFAULT_SESSION_OPTIONS = {
: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,
:session_http_only=> true
}
def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
@session_options = session_options
@env = env
@cgi = CGIWrapper.new(self)
super()
end
%w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER SCRIPT_NAME
SERVER_NAME SERVER_PROTOCOL
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
define_method(env.sub(/^HTTP_/n, '').downcase) do
@env[env]
end
end
def query_string
qs = super
if !qs.blank?
qs
else
@env['QUERY_STRING']
end
end
def body_stream #:nodoc:
@env['rack.input']
end
def key?(key)
@env.key?(key)
end
def cookies
return {} unless @env["HTTP_COOKIE"]
unless @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"]
@env["rack.request.cookie_string"] = @env["HTTP_COOKIE"]
@env["rack.request.cookie_hash"] = CGI::Cookie::parse(@env["rack.request.cookie_string"])
end
@env["rack.request.cookie_hash"]
end
def server_port
@env['SERVER_PORT'].to_i
end
def server_software
@env['SERVER_SOFTWARE'].split("/").first
end
def session
unless defined?(@session)
if @session_options == false
@session = Hash.new
else
stale_session_check! do
if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
raise SessionFixationAttempt
end
case value = session_options_with_string_keys['new_session']
when true
@session = new_session
when false
begin
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
# CGI::Session raises ArgumentError if 'new_session' == false
# and no session cookie or query param is present.
rescue ArgumentError
@session = Hash.new
end
when nil
@session = CGI::Session.new(@cgi, session_options_with_string_keys)
else
raise ArgumentError, "Invalid new_session option: #{value}"
end
@session['__valid_session']
end
end
end
@session
end
def reset_session
@session.delete if defined?(@session) && @session.is_a?(CGI::Session)
@session = new_session
end
private
# Delete an old session if it exists then create a new one.
def new_session
if @session_options == false
Hash.new
else
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
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:]*\w)}
begin
# 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.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
end
retry
else
raise
end
end
def session_options_with_string_keys
@session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
end
end
class RackResponse < AbstractResponse #:nodoc:
def initialize(request)
@cgi = request.cgi
@writer = lambda { |x| @body << x }
@block = nil
super()
end
# Retrieve status from instance variable if has already been delete
def status
@status || super
end
def out(output = $stdout, &block)
# Nasty hack because CGI sessions are closed after the normal
# prepare! statement
set_cookies!
@block = block
@status = headers.delete("Status")
if [204, 304].include?(status.to_i)
headers.delete("Content-Type")
[status, headers.to_hash, []]
else
[status, headers.to_hash, self]
end
end
alias to_a out
def each(&callback)
if @body.respond_to?(:call)
@writer = lambda { |x| callback.call(x) }
@body.call(self, self)
elsif @body.is_a?(String)
@body.each_line(&callback)
else
@body.each(&callback)
end
@writer = callback
@block.call(self) if @block
end
def write(str)
@writer.call str.to_s
str
end
def close
@body.close if @body.respond_to?(:close)
end
def empty?
@block == nil && @body.empty?
end
def prepare!
super
convert_language!
convert_expires!
set_status!
# set_cookies!
end
private
def convert_language!
headers["Content-Language"] = headers.delete("language") if headers["language"]
end
def convert_expires!
headers["Expires"] = headers.delete("") if headers["expires"]
end
def convert_content_type!
super
headers['Content-Type'] = headers.delete('type') || "text/html"
headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
end
def set_content_length!
super
headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
end
def set_status!
self.status ||= "200 OK"
end
def set_cookies!
# Convert 'cookie' header to 'Set-Cookie' headers.
# Because Set-Cookie header can appear more the once in the response body,
# we store it in a line break separated string that will be translated to
# multiple Set-Cookie header by the handler.
if cookie = headers.delete('cookie')
cookies = []
case cookie
when Array then cookie.each { |c| cookies << c.to_s }
when Hash then cookie.each { |_, c| cookies << c.to_s }
else cookies << cookie.to_s
end
@cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
end
end
end
class CGIWrapper < ::CGI
attr_reader :output_cookies
def initialize(request, *args)
@request = request
@args = *args
@input = request.body
super *args
end
def params
@params ||= @request.params
end
def cookies
@request.cookies
end
def query_string
@request.query_string
end
# Used to wrap the normal args variable used inside CGI.
def args
@args
end
# Used to wrap the normal env_table variable used inside CGI.
def env_table
@request.env
end
# Used to wrap the normal stdinput variable used inside CGI.
def stdinput
@input
end
end
end

View file

@ -0,0 +1,54 @@
require 'thread'
module ActionController
class Reloader
@@default_lock = Mutex.new
cattr_accessor :default_lock
class BodyWrapper
def initialize(body, lock)
@body = body
@lock = lock
end
def close
@body.close if @body.respond_to?(:close)
ensure
Dispatcher.cleanup_application
@lock.unlock
end
def method_missing(*args, &block)
@body.send(*args, &block)
end
def respond_to?(symbol, include_private = false)
symbol == :close || @body.respond_to?(symbol, include_private)
end
end
def self.run(lock = @@default_lock)
lock.lock
begin
Dispatcher.reload_application
status, headers, body = yield
# We do not want to call 'cleanup_application' in an ensure block
# because the returned Rack response body may lazily generate its data. This
# is for example the case if one calls
#
# render :text => lambda { ... code here which refers to application models ... }
#
# in an ActionController.
#
# Instead, we will want to cleanup the application code after the request is
# completely finished. So we wrap the body in a BodyWrapper class so that
# when the Rack handler calls #close during the end of the request, we get to
# run our cleanup code.
[status, headers, BodyWrapper.new(body, lock)]
rescue Exception
lock.unlock
raise
end
end
end
end

View file

@ -3,39 +3,42 @@ require 'stringio'
require 'strscan'
require 'active_support/memoizable'
require 'action_controller/cgi_ext'
module ActionController
# CgiRequest and TestRequest provide concrete implementations.
class AbstractRequest
extend ActiveSupport::Memoizable
class Request < Rack::Request
def self.relative_url_root=(relative_url_root)
ActiveSupport::Deprecation.warn(
"ActionController::AbstractRequest.relative_url_root= has been renamed." +
"You can now set it with config.action_controller.relative_url_root=", caller)
ActionController::Base.relative_url_root=relative_url_root
%w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
SERVER_NAME SERVER_PROTOCOL
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
define_method(env.sub(/^HTTP_/n, '').downcase) do
@env[env]
end
end
def key?(key)
@env.key?(key)
end
HTTP_METHODS = %w(get head put post delete options)
HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
# The hash of environment variables for this request,
# such as { 'RAILS_ENV' => 'production' }.
attr_reader :env
# The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
# Returns the true HTTP request \method as a lowercase symbol, such as
# <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS
# constant above, an UnknownHttpMethod exception is raised.
def request_method
method = @env['REQUEST_METHOD']
method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
@request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
end
memoize :request_method
# The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# Note, HEAD is returned as <tt>:get</tt> since the two are functionally
# equivalent from the application's perspective.
# Returns the HTTP request \method used for action processing as a
# lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
# method returns <tt>:get</tt> for a HEAD request because the two are
# functionally equivalent from the application's perspective.)
def method
request_method == :head ? :get : request_method
end
@ -70,43 +73,50 @@ module ActionController
#
# request.headers["Content-Type"] # => "text/plain"
def headers
ActionController::Http::Headers.new(@env)
@headers ||= ActionController::Http::Headers.new(@env)
end
memoize :headers
# Returns the content length of the request as an integer.
def content_length
@env['CONTENT_LENGTH'].to_i
super.to_i
end
memoize :content_length
# 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
Mime::Type.lookup(content_type_without_parameters)
@content_type ||= begin
if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
end
end
def media_type
content_type.to_s
end
memoize :content_type
# Returns the accepted MIME type for the request.
def accepts
header = @env['HTTP_ACCEPT'].to_s.strip
@accepts ||= begin
header = @env['HTTP_ACCEPT'].to_s.strip
if header.empty?
[content_type, Mime::ALL].compact
else
Mime::Type.parse(header)
if header.empty?
[content_type, Mime::ALL].compact
else
Mime::Type.parse(header)
end
end
end
memoize :accepts
def if_modified_since
if since = env['HTTP_IF_MODIFIED_SINCE']
Time.rfc2822(since) rescue nil
end
end
memoize :if_modified_since
def if_none_match
env['HTTP_IF_NONE_MATCH']
@ -125,15 +135,15 @@ module ActionController
# supplied, both must match, or the request is not considered fresh.
def fresh?(response)
case
when if_modified_since && if_none_match
not_modified?(response.last_modified) && etag_matches?(response.etag)
when if_modified_since
not_modified?(response.last_modified)
when if_none_match
etag_matches?(response.etag)
else
false
end
when if_modified_since && if_none_match
not_modified?(response.last_modified) && etag_matches?(response.etag)
when if_modified_since
not_modified?(response.last_modified)
when if_none_match
etag_matches?(response.etag)
else
false
end
end
# Returns the Mime type for the \format used in the request.
@ -209,7 +219,7 @@ module ActionController
# delimited list in the case of multiple chained proxies; the last
# address which is not trusted is the originating IP.
def remote_ip
remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].split(',').collect(&:strip)
remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)
unless remote_addr_list.blank?
not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
@ -218,7 +228,7 @@ module ActionController
remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
if @env.include? 'HTTP_CLIENT_IP'
if remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
# We don't know which came from the proxy, and which from the user
raise ActionControllerError.new(<<EOM)
IP spoofing attack?!
@ -240,26 +250,21 @@ EOM
@env['REMOTE_ADDR']
end
memoize :remote_ip
# 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
memoize :server_software
# Returns the complete URL used for this request.
def url
protocol + host_with_port + request_uri
end
memoize :url
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
def protocol
ssl? ? 'https://' : 'http://'
end
memoize :protocol
# Is this an SSL request?
def ssl?
@ -271,7 +276,7 @@ EOM
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
else
env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end
@ -279,14 +284,12 @@ EOM
def host
raw_host_with_port.sub(/:\d+$/, '')
end
memoize :host
# Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080".
def host_with_port
"#{host}#{port_string}"
end
memoize :host_with_port
# Returns the port number of this request as an integer.
def port
@ -296,7 +299,6 @@ EOM
standard_port
end
end
memoize :port
# Returns the standard \port number for this request's protocol.
def standard_port
@ -332,13 +334,8 @@ EOM
# Returns the query string, accounting for server idiosyncrasies.
def query_string
if uri = @env['REQUEST_URI']
uri.split('?', 2)[1] || ''
else
@env['QUERY_STRING'] || ''
end
@env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
end
memoize :query_string
# Returns the request URI, accounting for server idiosyncrasies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
@ -364,36 +361,33 @@ EOM
end
end
end
memoize :request_uri
# 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/^#{ActionController::Base.relative_url_root}/, '')
path || ''
path = request_uri.to_s[/\A[^\?]*/]
path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
path
end
memoize :path
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
unless env.include? 'RAW_POST_DATA'
env['RAW_POST_DATA'] = body.read(content_length)
unless @env.include? 'RAW_POST_DATA'
@env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
body.rewind if body.respond_to?(:rewind)
end
env['RAW_POST_DATA']
@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
alias_method :params, :parameters
def path_parameters=(parameters) #:nodoc:
@path_parameters = parameters
@env["action_controller.request.path_parameters"] = parameters
@symbolized_path_parameters = @parameters = nil
end
@ -409,464 +403,91 @@ EOM
#
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
@path_parameters ||= {}
@env["action_controller.request.path_parameters"] ||= {}
end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
def body
if raw_post = env['RAW_POST_DATA']
if raw_post = @env['RAW_POST_DATA']
raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
StringIO.new(raw_post)
else
body_stream
@env['rack.input']
end
end
def remote_addr
@env['REMOTE_ADDR']
def form_data?
FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
end
def referrer
@env['HTTP_REFERER']
# Override Rack's GET method to support indifferent access
def GET
@env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
end
alias referer referrer
alias_method :query_parameters, :GET
def query_parameters
@query_parameters ||= self.class.parse_query_parameters(query_string)
# Override Rack's POST method to support indifferent access
def POST
@env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
end
def request_parameters
@request_parameters ||= parse_formatted_request_parameters
end
#--
# Must be implemented in the concrete request
#++
alias_method :request_parameters, :POST
def body_stream #:nodoc:
@env['rack.input']
end
def cookies #:nodoc:
end
def session #:nodoc:
def session
@env['rack.session'] ||= {}
end
def session=(session) #:nodoc:
@session = session
@env['rack.session'] = session
end
def reset_session #:nodoc:
def reset_session
@env['rack.session.options'].delete(:id)
@env['rack.session'] = {}
end
protected
# The raw content type string. Use when you need parameters such as
# charset or boundary which aren't included in the content_type MIME type.
# Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
def content_type_with_parameters
content_type_from_legacy_post_data_format_header ||
env['CONTENT_TYPE'].to_s
end
def session_options
@env['rack.session.options'] ||= {}
end
# The raw content type string with its parameters stripped off.
def content_type_without_parameters
self.class.extract_content_type_without_parameters(content_type_with_parameters)
end
memoize :content_type_without_parameters
def session_options=(options)
@env['rack.session.options'] = options
end
def server_port
@env['SERVER_PORT'].to_i
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)
when :json
if body.blank?
{}
else
data = ActiveSupport::JSON.decode(body)
data = {:_json => data} unless data.is_a?(Hash)
data.with_indifferent_access
end
else
{}
end
rescue Exception => e # YAML, XML or Ruby code block errors
raise
{ "body" => body,
"content_type" => content_type_with_parameters,
"content_length" => content_length,
"exception" => "#{e.message} (#{e.class})",
"backtrace" => e.backtrace }
end
def named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end
class << self
def parse_query_parameters(query_string)
return {} if query_string.blank?
pairs = query_string.split('&').collect do |chunk|
next if chunk.empty?
key, value = chunk.split('=', 2)
next if key.empty?
value = value.nil? ? nil : CGI.unescape(value)
[ CGI.unescape(key), value ]
end.compact
UrlEncodedPairParser.new(pairs).result
end
def parse_request_parameters(params)
parser = UrlEncodedPairParser.new
params = params.dup
until params.empty?
for key, value in params
if key.blank?
params.delete key
elsif !key.include?('[')
# much faster to test for the most common case first (GET)
# and avoid the call to build_deep_hash
parser.result[key] = get_typed_value(value[0])
params.delete key
elsif value.is_a?(Array)
parser.parse(key, get_typed_value(value.shift))
params.delete key if value.empty?
else
raise TypeError, "Expected array, found #{value.inspect}"
end
end
end
parser.result
end
def parse_multipart_form_parameters(body, boundary, body_size, env)
parse_request_parameters(read_multipart(body, boundary, body_size, env))
end
def extract_multipart_boundary(content_type_with_parameters)
if content_type_with_parameters =~ MULTIPART_BOUNDARY
['multipart/form-data', $1.dup]
else
extract_content_type_without_parameters(content_type_with_parameters)
end
end
def extract_content_type_without_parameters(content_type_with_parameters)
$1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
end
def clean_up_ajax_request_body!(body)
body.chop! if body[-1] == 0
body.gsub!(/&_=$/, '')
end
private
def get_typed_value(value)
case value
when String
value
when NilClass
''
when Array
value.map { |v| get_typed_value(v) }
else
if value.respond_to? :original_filename
# Uploaded file
if value.original_filename
value
# Multipart param
else
result = value.read
value.rewind
result
end
# Unknown value, neither string nor multipart.
else
raise "Unknown form value: #{value.inspect}"
end
end
end
MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
EOL = "\015\012"
def read_multipart(body, boundary, body_size, env)
params = Hash.new([])
boundary = "--" + boundary
quoted_boundary = Regexp.quote(boundary)
buf = ""
bufsize = 10 * 1024
boundary_end=""
# start multipart/form-data
body.binmode if defined? body.binmode
case body
when File
body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
when StringIO
body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
end
boundary_size = boundary.size + EOL.size
body_size -= boundary_size
status = body.read(boundary_size)
if nil == status
raise EOFError, "no content body"
elsif boundary + EOL != status
raise EOFError, "bad content body"
end
loop do
head = nil
content =
if 10240 < body_size
UploadedTempfile.new("CGI")
else
UploadedStringIO.new
end
content.binmode if defined? content.binmode
until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
if (not head) and /#{EOL}#{EOL}/n.match(buf)
buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
head = $1.dup
""
end
next
end
if head and ( (EOL + boundary + EOL).size < buf.size )
content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
end
c = if bufsize < body_size
body.read(bufsize)
else
body.read(body_size)
end
if c.nil? || c.empty?
raise EOFError, "bad content body"
end
buf.concat(c)
body_size -= c.size
end
buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
content.print $1
if "--" == $2
body_size = -1
end
boundary_end = $2.dup
""
end
content.rewind
head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
if filename = $1 || $2
if /Mac/ni.match(env['HTTP_USER_AGENT']) and
/Mozilla/ni.match(env['HTTP_USER_AGENT']) and
(not /MSIE/ni.match(env['HTTP_USER_AGENT']))
filename = CGI.unescape(filename)
end
content.original_path = filename.dup
end
head =~ /Content-Type: ([^\r]*)/ni
content.content_type = $1.dup if $1
head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
name = $1.dup if $1
if params.has_key?(name)
params[name].push(content)
else
params[name] = [content]
end
break if body_size == -1
end
raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
begin
body.rewind if body.respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
params
end
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
value = top[key]
end
# Convert nested Hashs to HashWithIndifferentAccess and replace
# file upload hashs with UploadedFile objects
def normalize_parameters(value)
case value
when Hash
if value.has_key?(:tempfile)
upload = value[:tempfile]
upload.extend(UploadedFile)
upload.original_path = value[:filename]
upload.content_type = value[:type]
upload
else
top << value
h = {}
value.each { |k, v| h[k] = normalize_parameters(v) }
h.with_indifferent_access
end
elsif top.is_a? Hash
key = CGI.unescape(key)
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
top[key] ||= value
return top[key]
when Array
value.map { |e| normalize_parameters(e) }
else
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
value
end
return value
end
def type_conflict!(klass, value)
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
end
end
module UploadedFile
def self.included(base)
base.class_eval do
attr_accessor :original_path, :content_type
alias_method :local_path, :path
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

@ -5,8 +5,6 @@ module ActionController #:nodoc:
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
@ -14,7 +12,7 @@ module ActionController #:nodoc:
end
# Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current web application, not a
# forged link from another site, is done by embedding a token based on the session (which an attacker wouldn't know) in all
# forged link from another site, is done by embedding a token based on a random string stored in 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 idempotent anyway.
@ -57,12 +55,8 @@ module ActionController #:nodoc:
# 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
@ -70,13 +64,12 @@ module ActionController #:nodoc:
# Valid Options:
#
# * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified.
# * <tt>:secret</tt> - Custom salt used to generate the <tt>form_authenticity_token</tt>.
# 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)
if options[:secret] || options[:digest]
ActiveSupport::Deprecation.warn("protect_from_forgery only takes :only and :except options now. :digest and :secret have no effect", caller)
end
end
end
@ -88,16 +81,21 @@ module ActionController #:nodoc:
# Returns true or false if a request is verified. Checks:
#
# * is the format restricted? By default, only HTML and AJAX requests are checked.
# * is the format restricted? By default, only HTML 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?
# * Does the form_authenticity_token match the given token value from the params?
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
request.xhr? ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
form_authenticity_token == form_authenticity_param
end
def form_authenticity_param
params[request_forgery_protection_token]
end
def verifiable_request_format?
!request.content_type.nil? && request.content_type.verify_request?
end
@ -105,34 +103,9 @@ module ActionController #:nodoc:
# Sets the token value for the current session. Pass a <tt>:secret</tt> option
# in +protect_from_forgery+ to add a custom salt to the hash.
def form_authenticity_token
@form_authenticity_token ||= if !session.respond_to?(:session_id)
raise InvalidAuthenticityToken, "Request Forgery Protection requires a valid session. Use #allow_forgery_protection to disable it, or use a valid session."
elsif 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
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
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
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

View file

@ -1,169 +0,0 @@
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(script_path)
reset!
end
def benchmark(n, profiling = false)
@quiet = true
print ' '
result = Benchmark.realtime do
n.times do |i|
run(profiling)
print_progress(i)
end
end
puts
result
ensure
@quiet = false
end
def say(message)
puts " #{message}" unless @quiet
end
private
def define_run_method(script_path)
script = File.read(script_path)
source = <<-end_source
def run(profiling = false)
if profiling
RubyProf.resume do
#{script}
end
else
#{script}
end
old_request_count = request_count
reset!
self.request_count = old_request_count
end
end_source
instance_eval source, script_path, 1
end
def print_progress(i)
print "\n " if i % 60 == 0
print ' ' if i % 10 == 0
print '.'
$stdout.flush
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
benchmark(sandbox, true)
results = RubyProf.stop
show_profile_results results
results
end
def benchmark(sandbox, profiling = false)
sandbox.request_count = 0
elapsed = sandbox.benchmark(options[:n], profiling).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(false) }
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 [100]', 'How many requests to process. Defaults to 100.') { |v| options[:n] = v.to_i if v }
opt.on('-b', '--benchmark', 'Benchmark instead of profiling') { |v| options[:benchmark] = v }
opt.on('-m', '--measure [mode]', 'Which ruby-prof measure mode to use: process_time, wall_time, cpu_time, allocations, or memory. Defaults to process_time.') { |v| options[:measure] = 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
gem 'ruby-prof', '>= 0.6.1'
require 'ruby-prof'
if mode = options[:measure]
RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
end
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,13 +1,19 @@
module ActionController #:nodoc:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
# is already implemented by the Action Controller, but the public view should be tailored to your specific application.
#
# The default behavior for public exceptions is to render a static html file with the name of the error code thrown. If no such
# file exists, an empty response is sent with the correct status code.
# 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.
#
# 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.
# The default behavior for public exceptions is to render a static html
# file with the name of the error code thrown. If no such file exists, an
# empty response is sent with the correct status code.
#
# You can override what constitutes a local request by overriding the
# <tt>local_request?</tt> method in your own controller. Custom rescue
# behavior is achieved by overriding the <tt>rescue_action_in_public</tt>
# and <tt>rescue_action_locally</tt> methods.
module Rescue
LOCALHOST = '127.0.0.1'.freeze
@ -32,6 +38,9 @@ module ActionController #:nodoc:
'ActionView::TemplateError' => 'template_error'
}
RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new_and_loaded(
File.join(File.dirname(__FILE__), "templates"))
def self.included(base) #:nodoc:
base.cattr_accessor :rescue_responses
base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
@ -50,47 +59,60 @@ module ActionController #:nodoc:
end
module ClassMethods
def process_with_exception(request, response, exception) #:nodoc:
def call_with_exception(env, exception) #:nodoc:
request = env["action_controller.rescue.request"] ||= Request.new(env)
response = env["action_controller.rescue.response"] ||= Response.new
new.process(request, response, :rescue_action, exception)
end
end
protected
# Exception handler called when the performance of an action raises an exception.
# Exception handler called when the performance of an action raises
# an exception.
def rescue_action(exception)
rescue_with_handler(exception) || rescue_action_without_handler(exception)
rescue_with_handler(exception) ||
rescue_action_without_handler(exception)
end
# Overwrite to implement custom logging of errors. By default logs as fatal.
# Overwrite to implement custom logging of errors. By default
# logs as fatal.
def log_error(exception) #:doc:
ActiveSupport::Deprecation.silence do
if ActionView::TemplateError === exception
logger.fatal(exception.to_s)
else
logger.fatal(
"\n\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") +
"\n\n"
"\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") + "\n\n"
)
end
end
end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). By
# default will call render_optional_error_file. Override this method to provide more user friendly error messages.
# Overwrite to implement public exception handling (for requests
# answering false to <tt>local_request?</tt>). By default will call
# render_optional_error_file. Override this method to provide more
# user friendly error messages.
def rescue_action_in_public(exception) #:doc:
render_optional_error_file response_code_for_rescue(exception)
end
# Attempts to render a static error page based on the <tt>status_code</tt> thrown,
# or just return headers if no such file exists. 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.
# Attempts to render a static error page based on the
# <tt>status_code</tt> thrown, or just return headers if no such file
# exists. At first, it will try to render a localized static page.
# For example, if a 500 error is being handled Rails and locale is :da,
# it will first attempt to render the file at <tt>public/500.da.html</tt>
# then attempt to render <tt>public/500.html</tt>. If none of them exist,
# the body of the response will be left empty.
def render_optional_error_file(status_code)
status = interpret_status(status_code)
locale_path = "#{Rails.public_path}/#{status[0,3]}.#{I18n.locale}.html" if I18n.locale
path = "#{Rails.public_path}/#{status[0,3]}.html"
if File.exist?(path)
render :file => path, :status => status
if locale_path && File.exist?(locale_path)
render :file => locale_path, :status => status, :content_type => Mime::HTML
elsif File.exist?(path)
render :file => path, :status => status, :content_type => Mime::HTML
else
head status
end
@ -107,11 +129,13 @@ module ActionController #:nodoc:
# a controller action.
def rescue_action_locally(exception)
@template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception)))
@template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH)
@template.instance_variable_set("@contents",
@template.render(:file => template_path_for_local_rescue(exception)))
response.content_type = Mime::HTML
render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
render_for_file(rescues_path("layout"),
response_code_for_rescue(exception))
end
def rescue_action_without_handler(exception)
@ -139,7 +163,7 @@ module ActionController #:nodoc:
end
def rescues_path(template_name)
"#{File.dirname(__FILE__)}/templates/rescues/#{template_name}.erb"
RESCUES_TEMPLATE_PATH["rescues/#{template_name}.erb"]
end
def template_path_for_local_rescue(exception)
@ -151,13 +175,9 @@ module ActionController #:nodoc:
end
def clean_backtrace(exception)
if backtrace = exception.backtrace
if defined?(RAILS_ROOT)
backtrace.map { |line| line.sub RAILS_ROOT, '' }
else
backtrace
end
end
defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
Rails.backtrace_cleaner.clean(exception.backtrace) :
exception.backtrace
end
end
end

View file

@ -42,7 +42,7 @@ module ActionController
#
# Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer
module Resources
INHERITABLE_OPTIONS = :namespace, :shallow, :actions
INHERITABLE_OPTIONS = :namespace, :shallow
class Resource #:nodoc:
DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy
@ -91,7 +91,7 @@ module ActionController
end
def shallow_path_prefix
@shallow_path_prefix ||= "#{path_prefix unless @options[:shallow]}"
@shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix
end
def member_path
@ -103,7 +103,7 @@ module ActionController
end
def shallow_name_prefix
@shallow_name_prefix ||= "#{name_prefix unless @options[:shallow]}"
@shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix
end
def nesting_name_prefix
@ -119,7 +119,7 @@ module ActionController
end
def has_action?(action)
!DEFAULT_ACTIONS.include?(action) || @options[:actions].nil? || @options[:actions].include?(action)
!DEFAULT_ACTIONS.include?(action) || action_allowed?(action)
end
protected
@ -135,22 +135,27 @@ module ActionController
end
def set_allowed_actions
only = @options.delete(:only)
except = @options.delete(:except)
only, except = @options.values_at(:only, :except)
@allowed_actions ||= {}
if only && except
raise ArgumentError, 'Please supply either :only or :except, not both.'
elsif only == :all || except == :none
options[:actions] = DEFAULT_ACTIONS
if only == :all || except == :none
only = nil
except = []
elsif only == :none || except == :all
options[:actions] = []
elsif only
options[:actions] = DEFAULT_ACTIONS & Array(only).map(&:to_sym)
elsif except
options[:actions] = DEFAULT_ACTIONS - Array(except).map(&:to_sym)
else
# leave options[:actions] alone
only = []
except = nil
end
if only
@allowed_actions[:only] = Array(only).map(&:to_sym)
elsif except
@allowed_actions[:except] = Array(except).map(&:to_sym)
end
end
def action_allowed?(action)
only, except = @allowed_actions.values_at(:only, :except)
(!only || only.include?(action)) && (!except || !except.include?(action))
end
def set_prefixes
@ -283,7 +288,12 @@ module ActionController
# * <tt>:new</tt> - Same as <tt>:collection</tt>, 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>:requirements</tt> - Set custom routing parameter requirements; this is a hash of either
# regular expressions (which must match for the route to match) or extra parameters. For example:
#
# map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' }
#
# will only match if the first part is alphabetic, and will pass the parameter :extra to the controller.
# * <tt>:conditions</tt> - Specify custom routing recognition conditions. \Resources sets the <tt>:method</tt> value for the method-specific routes.
# * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example:
# # products_path == '/productos'
@ -307,9 +317,10 @@ module ActionController
# notes.resources :attachments
# end
#
# * <tt>:path_names</tt> - Specify different names for the 'new' and 'edit' actions. For example:
# * <tt>:path_names</tt> - Specify different path names for the actions. For example:
# # new_products_path == '/productos/nuevo'
# map.resources :products, :as => 'productos', :path_names => { :new => 'nuevo', :edit => 'editar' }
# # bids_product_path(1) == '/productos/1/licitacoes'
# map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' }
#
# You can also set default action names from an environment, like this:
# config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' }
@ -398,8 +409,6 @@ module ActionController
# # --> POST /posts/1/comments (maps to the CommentsController#create action)
# # --> PUT /posts/1/comments/1 (fails)
#
# The <tt>:only</tt> and <tt>:except</tt> options are inherited by any nested resource(s).
#
# If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
#
# Examples:
@ -517,16 +526,16 @@ module ActionController
resource = Resource.new(entities, options)
with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_associations(resource, options)
if block_given?
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
end
end
@ -534,16 +543,16 @@ module ActionController
resource = SingletonResource.new(entities, options)
with_options :controller => resource.controller do |map|
map_collection_actions(map, resource)
map_default_singleton_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_associations(resource, options)
if block_given?
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end
map_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_default_singleton_actions(map, resource)
end
end
@ -578,7 +587,10 @@ module ActionController
resource.collection_methods.each do |method, actions|
actions.each do |action|
[method].flatten.each do |m|
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
action_path ||= action
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
end
end
end
@ -622,7 +634,7 @@ module ActionController
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
action_path ||= Base.resources_path_names[action] || action
map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m)
map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true })
end
end
end
@ -633,16 +645,14 @@ module ActionController
map_resource_routes(map, resource, :destroy, resource.member_path, route_path)
end
def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil)
def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} )
if resource.has_action?(action)
action_options = action_options_for(action, resource, method)
action_options = action_options_for(action, resource, method, resource_options)
formatted_route_path = "#{route_path}.:format"
if route_name && @set.named_routes[route_name.to_sym].nil?
map.named_route(route_name, route_path, action_options)
map.named_route("formatted_#{route_name}", formatted_route_path, action_options)
map.named_route(route_name, formatted_route_path, action_options)
else
map.connect(route_path, action_options)
map.connect(formatted_route_path, action_options)
end
end
@ -654,9 +664,10 @@ module ActionController
end
end
def action_options_for(action, resource, method = nil)
def action_options_for(action, resource, method = nil, resource_options = {})
default_options = { :action => action.to_s }
require_id = !resource.kind_of?(SingletonResource)
force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource)
case default_options[:action]
when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
@ -664,12 +675,8 @@ module ActionController
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)
else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id))
end
end
end
end
class ActionController::Routing::RouteSet::Mapper
include ActionController::Resources
end

View file

@ -1,24 +1,25 @@
require 'digest/md5'
module ActionController # :nodoc:
# Represents an HTTP response generated by a controller action. One can use an
# ActionController::AbstractResponse object to retrieve the current state of the
# response, or customize the response. An AbstractResponse object can either
# represent a "real" HTTP response (i.e. one that is meant to be sent back to the
# web browser) or a test response (i.e. one that is generated from integration
# tests). See CgiResponse and TestResponse, respectively.
# Represents an HTTP response generated by a controller action. One can use
# an ActionController::Response object to retrieve the current state
# of the response, or customize the response. An Response object can
# either represent a "real" HTTP response (i.e. one that is meant to be sent
# back to the web browser) or a test response (i.e. one that is generated
# from integration tests). See CgiResponse and TestResponse, respectively.
#
# AbstractResponse is mostly a Ruby on Rails framework implement detail, and should
# never be used directly in controllers. Controllers should use the methods defined
# in ActionController::Base instead. For example, if you want to set the HTTP
# response's content MIME type, then use ActionControllerBase#headers instead of
# AbstractResponse#headers.
# Response is mostly a Ruby on Rails framework implement detail, and
# should never be used directly in controllers. Controllers should use the
# methods defined in ActionController::Base instead. For example, if you want
# to set the HTTP response's content MIME type, then use
# ActionControllerBase#headers instead of Response#headers.
#
# Nevertheless, integration tests may want to inspect controller responses in more
# detail, and that's when AbstractResponse can be useful for application developers.
# Integration test methods such as ActionController::Integration::Session#get and
# ActionController::Integration::Session#post return objects of type TestResponse
# (which are of course also of type AbstractResponse).
# Nevertheless, integration tests may want to inspect controller responses in
# more detail, and that's when Response can be useful for application
# developers. Integration test methods such as
# ActionController::Integration::Session#get and
# ActionController::Integration::Session#post return objects of type
# TestResponse (which are of course also of type Response).
#
# For example, the following demo integration "test" prints the body of the
# controller response to the console:
@ -29,25 +30,26 @@ module ActionController # :nodoc:
# puts @response.body
# end
# end
class AbstractResponse
class Response < Rack::Response
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :request
# The body content (e.g. HTML) of the response, as a String.
attr_accessor :body
# The headers of the response, as a Hash. It maps header names to header values.
attr_accessor :headers
attr_accessor :session, :cookies, :assigns, :template, :layout
attr_accessor :session, :assigns, :template, :layout
attr_accessor :redirected_to, :redirected_to_method_params
delegate :default_charset, :to => 'ActionController::Base'
def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end
@status = 200
@header = Rack::Utils::HeaderHash.new(DEFAULT_HEADERS)
def status; headers['Status'] end
def status=(status) headers['Status'] = status end
@writer = lambda { |x| @body << x }
@block = nil
@body = "",
@session = []
@assigns = []
end
def location; headers['Location'] end
def location=(url) headers['Location'] = url end
@ -109,13 +111,17 @@ module ActionController # :nodoc:
def etag
headers['ETag']
end
def etag?
headers.include?('ETag')
end
def etag=(etag)
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
if etag.blank?
headers.delete('ETag')
else
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
end
end
def redirect(url, status)
@ -138,26 +144,64 @@ module ActionController # :nodoc:
handle_conditional_get!
set_content_length!
convert_content_type!
convert_language!
convert_cookies!
end
def each(&callback)
if @body.respond_to?(:call)
@writer = lambda { |x| callback.call(x) }
@body.call(self, self)
elsif @body.respond_to?(:to_str)
yield @body
else
@body.each(&callback)
end
@writer = callback
@block.call(self) if @block
end
def write(str)
@writer.call str.to_s
str
end
def flush #:nodoc:
ActiveSupport::Deprecation.warn(
'Calling output.flush is no longer needed for streaming output ' +
'because ActionController::Response automatically handles it', caller)
end
def set_cookie(key, value)
if value.has_key?(:http_only)
ActiveSupport::Deprecation.warn(
"The :http_only option in ActionController::Response#set_cookie " +
"has been renamed. Please use :httponly instead.", caller)
value[:httponly] ||= value.delete(:http_only)
end
super(key, value)
end
private
def handle_conditional_get!
if etag? || last_modified?
set_conditional_cache_control!
elsif nonempty_ok_response?
self.etag = body
def handle_conditional_get!
if etag? || last_modified?
set_conditional_cache_control!
elsif nonempty_ok_response?
self.etag = body
if request && request.etag_matches?(etag)
self.status = '304 Not Modified'
self.body = ''
end
if request && request.etag_matches?(etag)
self.status = '304 Not Modified'
self.body = ''
end
set_conditional_cache_control!
end
set_conditional_cache_control!
end
end
def nonempty_ok_response?
ok = !status || status[0..2] == '200'
ok = !status || status.to_s[0..2] == '200'
ok && body.is_a?(String) && !body.empty?
end
@ -168,23 +212,28 @@ module ActionController # :nodoc:
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
headers['Content-Type'] ||= "text/html"
headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
end
# Don't set the Content-Length for block-based bodies as that would mean
# reading it all into memory. Not nice for, say, a 2GB streaming file.
def set_content_length!
if status && status.to_s[0..2] == '204'
headers.delete('Content-Length')
elsif length = headers['Content-Length']
headers['Content-Length'] = length.to_s
elsif !body.respond_to?(:call) && (!status || status.to_s[0..2] != '304')
headers["Content-Length"] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
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!
unless body.respond_to?(:call) || (status && status[0..2] == '304')
self.headers["Content-Length"] ||= body.size
end
def convert_language!
headers["Content-Language"] = headers.delete("language") if headers["language"]
end
def convert_cookies!
headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact
end
end
end

View file

@ -1,6 +1,5 @@
require 'cgi'
require 'uri'
require 'action_controller/polymorphic_routes'
require 'action_controller/routing/optimisations'
require 'action_controller/routing/routing_ext'
require 'action_controller/routing/route'
@ -84,9 +83,11 @@ module ActionController
# This sets up +blog+ as the default controller if no other is specified.
# This means visiting '/' would invoke the blog controller.
#
# More formally, you can define defaults in a route with the <tt>:defaults</tt> key.
# More formally, you can include arbitrary parameters in the route, thus:
#
# map.connect ':controller/:action/:id', :action => 'show', :defaults => { :page => 'Dashboard' }
# map.connect ':controller/:action/:id', :action => 'show', :page => 'Dashboard'
#
# This will pass the :page parameter to all incoming requests that match this route.
#
# Note: The default routes, as provided by the Rails generator, make all actions in every
# controller accessible via GET requests. You should consider removing them or commenting
@ -192,9 +193,8 @@ module ActionController
#
# map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
#
# will glob all remaining parts of the route that were not recognized earlier. This idiom
# must appear at the end of the path. The globbed values are in <tt>params[:path]</tt> in
# this case.
# will glob all remaining parts of the route that were not recognized earlier.
# The globbed values are in <tt>params[:path]</tt> as an array of path segments.
#
# == Route conditions
#
@ -267,10 +267,13 @@ module ActionController
module Routing
SEPARATORS = %w( / . ? )
HTTP_METHODS = [:get, :head, :post, :put, :delete]
HTTP_METHODS = [:get, :head, :post, :put, :delete, :options]
ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set
mattr_accessor :generate_best_match
self.generate_best_match = true
# The root paths which may contain controller files
mattr_accessor :controller_paths
self.controller_paths = []

View file

@ -34,6 +34,8 @@ module ActionController
def segment_for(string)
segment =
case string
when /\A\.(:format)?\//
OptionalFormatSegment.new
when /\A:(\w+)/
key = $1.to_sym
key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key)
@ -157,7 +159,8 @@ module ActionController
path = "/#{path}" unless path[0] == ?/
path = "#{path}/" unless path[-1] == ?/
path = "/#{options[:path_prefix].to_s.gsub(/^\//,'')}#{path}" if options[:path_prefix]
prefix = options[:path_prefix].to_s.gsub(/^\//,'')
path = "/#{prefix}#{path}" unless prefix.blank?
segments = segments_for_route_path(path)
defaults, requirements, conditions = divide_route_options(segments, options)

View file

@ -65,7 +65,7 @@ module ActionController
# rather than triggering the expensive logic in +url_for+.
class PositionalArguments < Optimiser
def guard_conditions
number_of_arguments = route.segment_keys.size
number_of_arguments = route.required_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

View file

@ -56,7 +56,7 @@ module ActionController
result = recognize_optimized(path, environment) and return result
# Route was not recognized. Try to find out why (maybe wrong verb).
allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(:method => verb)) } }
if environment[:method] && !HTTP_METHODS.include?(environment[:method])
raise NotImplemented.new(*allows)
@ -98,7 +98,6 @@ module ActionController
if Array === item
i += 1
start = (i == 1)
final = (i == list.size)
tag, sub = item
if tag == :dynamic
body += padding + "#{start ? 'if' : 'elsif'} true\n"

View file

@ -35,6 +35,11 @@ module ActionController
segment.key if segment.respond_to? :key
end.compact
end
def required_segment_keys
required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) }
required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact
end
# Build a query string from the keys of the given hash. If +only_keys+
# is given (as an array), only the keys indicated will be used to build
@ -122,6 +127,16 @@ module ActionController
super
end
def generate(options, hash, expire_on = {})
path, hash = generate_raw(options, hash, expire_on)
append_query_string(path, hash, extra_keys(options))
end
def generate_extras(options, hash, expire_on = {})
path, hash = generate_raw(options, hash, expire_on)
[path, extra_keys(options)]
end
private
def requirement_for(key)
return requirements[key] if requirements.key? key
@ -150,11 +165,6 @@ module ActionController
# the query string. (Never use keys from the recalled request when building the
# query string.)
method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
raw_method
end

View file

@ -7,6 +7,8 @@ module ActionController
# Mapper instances have relatively few instance methods, in order to avoid
# clashes with named routes.
class Mapper #:doc:
include ActionController::Resources
def initialize(set) #:nodoc:
@set = set
end
@ -136,13 +138,17 @@ module ActionController
end
end
def named_helper_module_eval(code, *args)
@module.module_eval(code, *args)
end
def define_hash_access(route, name, kind, options)
selector = hash_access_name(name, kind)
@module.module_eval <<-end_eval # We use module_eval to avoid leaks
def #{selector}(options = nil)
options ? #{options.inspect}.merge(options) : #{options.inspect}
end
protected :#{selector}
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
def #{selector}(options = nil) # def hash_for_users_url(options = nil)
options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
end # end
protected :#{selector} # protected :hash_for_users_url
end_eval
helpers << selector
end
@ -166,33 +172,44 @@ module ActionController
#
# foo_url(bar, baz, bang, :sort_by => 'baz')
#
@module.module_eval <<-end_eval # We use module_eval to avoid leaks
def #{selector}(*args)
#{generate_optimisation_block(route, kind)}
opts = if args.empty? || Hash === args.first
args.first || {}
else
options = args.extract_options!
args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
h[k] = v
h
end
options.merge(args)
end
url_for(#{hash_access_method}(opts))
end
protected :#{selector}
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
def #{selector}(*args) # def users_url(*args)
#
#{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
#
opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first
args.first || {} # args.first || {}
else # else
options = args.extract_options! # options = args.extract_options!
args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)|
h[k] = v # h[k] = v
h # h
end # end
options.merge(args) # options.merge(args)
end # end
#
url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts))
#
end # end
#Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL.
def formatted_#{selector}(*args) # def formatted_users_url(*args)
ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn(
"formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " +
"Please pass format to the standard " + # "Please pass format to the standard " +
"#{selector} method instead.", caller) # "users_url method instead.", caller)
#{selector}(*args) # users_url(*args)
end # end
protected :#{selector} # protected :users_url
end_eval
helpers << selector
end
end
attr_accessor :routes, :named_routes, :configuration_file
attr_accessor :routes, :named_routes, :configuration_files
def initialize
self.configuration_files = []
self.routes = []
self.named_routes = NamedRouteCollection.new
@ -206,7 +223,6 @@ module ActionController
end
def draw
clear!
yield Mapper.new(self)
install_helpers
end
@ -230,8 +246,22 @@ module ActionController
routes.empty?
end
def add_configuration_file(path)
self.configuration_files << path
end
# Deprecated accessor
def configuration_file=(path)
add_configuration_file(path)
end
# Deprecated accessor
def configuration_file
configuration_files
end
def load!
Routing.use_controllers! nil # Clear the controller cache so we may discover new ones
Routing.use_controllers!(nil) # Clear the controller cache so we may discover new ones
clear!
load_routes!
end
@ -240,26 +270,42 @@ module ActionController
alias reload! load!
def reload
if @routes_last_modified && configuration_file
mtime = File.stat(configuration_file).mtime
# if it hasn't been changed, then just return
return if mtime == @routes_last_modified
# if it has changed then record the new time and fall to the load! below
@routes_last_modified = mtime
if configuration_files.any? && @routes_last_modified
if routes_changed_at == @routes_last_modified
return # routes didn't change, don't reload
else
@routes_last_modified = routes_changed_at
end
end
load!
end
def load_routes!
if configuration_file
load configuration_file
@routes_last_modified = File.stat(configuration_file).mtime
if configuration_files.any?
configuration_files.each { |config| load(config) }
@routes_last_modified = routes_changed_at
else
add_route ":controller/:action/:id"
end
end
def routes_changed_at
routes_changed_at = nil
configuration_files.each do |config|
config_changed_at = File.stat(config).mtime
if routes_changed_at.nil? || config_changed_at > routes_changed_at
routes_changed_at = config_changed_at
end
end
routes_changed_at
end
def add_route(path, options = {})
options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) }
route = builder.build(path, options)
routes << route
route
@ -359,11 +405,14 @@ module ActionController
end
# don't use the recalled keys when determining which routes to check
routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
future_routes, deprecated_routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }]
routes = Routing.generate_best_match ? deprecated_routes : future_routes
routes.each do |route|
routes.each_with_index do |route, index|
results = route.__send__(method, options, merged, expire_on)
return results if results && (!results.is_a?(Array) || results.first)
if results && (!results.is_a?(Array) || results.first)
return results
end
end
end
@ -382,10 +431,16 @@ module ActionController
end
end
def call(env)
request = Request.new(env)
app = Routing::Routes.recognize(request)
app.call(env).to_a
end
def recognize(request)
params = recognize_path(request.path, extract_request_environment(request))
request.path_parameters = params.with_indifferent_access
"#{params[:controller].camelize}Controller".constantize
"#{params[:controller].to_s.camelize}Controller".constantize
end
def recognize_path(path, environment={})
@ -396,7 +451,10 @@ module ActionController
@routes_by_controller ||= Hash.new do |controller_hash, controller|
controller_hash[controller] = Hash.new do |action_hash, action|
action_hash[action] = Hash.new do |key_hash, keys|
key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys)
key_hash[keys] = [
routes_for_controller_and_action_and_keys(controller, action, keys),
deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
]
end
end
end
@ -408,10 +466,11 @@ module ActionController
merged = options if expire_on[:controller]
action = merged[:action] || 'index'
routes_by_controller[controller][action][merged.keys]
routes_by_controller[controller][action][merged.keys][1]
end
def routes_for_controller_and_action(controller, action)
ActiveSupport::Deprecation.warn "routes_for_controller_and_action() has been deprecated. Please use routes_for()"
selected = routes.select do |route|
route.matches_controller_and_action? controller, action
end
@ -419,6 +478,12 @@ module ActionController
end
def routes_for_controller_and_action_and_keys(controller, action, keys)
routes.select do |route|
route.matches_controller_and_action? controller, action
end
end
def deprecated_routes_for_controller_and_action_and_keys(controller, action, keys)
selected = routes.select do |route|
route.matches_controller_and_action? controller, action
end

View file

@ -3,7 +3,11 @@ module ActionController
class Segment #:nodoc:
RESERVED_PCHAR = ':@&=+$,;'
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
if RUBY_VERSION >= '1.9'
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
else
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
end
# TODO: Convert :is_optional accessor to read only
attr_accessor :is_optional
@ -191,23 +195,19 @@ module ActionController
end
def regexp_chunk
if regexp
if regexp_has_modifiers?
"(#{regexp.to_s})"
else
"(#{regexp.source})"
end
else
"([^#{Routing::SEPARATORS.join}]+)"
end
regexp ? regexp_string : default_regexp_chunk
end
def regexp_string
regexp_has_modifiers? ? "(#{regexp.to_s})" : "(#{regexp.source})"
end
def default_regexp_chunk
"([^#{Routing::SEPARATORS.join}]+)"
end
def number_of_captures
if regexp
regexp.number_of_captures + 1
else
1
end
regexp ? regexp.number_of_captures + 1 : 1
end
def build_pattern(pattern)
@ -244,10 +244,6 @@ module ActionController
"(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
end
def number_of_captures
1
end
# Don't URI.escape the controller name since it may contain slashes.
def interpolation_chunk(value_code = local_name)
"\#{#{value_code}.to_s}"
@ -289,8 +285,8 @@ module ActionController
"params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
end
def regexp_chunk
regexp || "(.*)"
def default_regexp_chunk
"(.*)"
end
def number_of_captures
@ -308,5 +304,40 @@ module ActionController
end
end
end
# The OptionalFormatSegment allows for any resource route to have an optional
# :format, which decreases the amount of routes created by 50%.
class OptionalFormatSegment < DynamicSegment
def initialize(key = nil, options = {})
super(:format, {:optional => true}.merge(options))
end
def interpolation_chunk
"." + super
end
def regexp_chunk
'/|(\.[^/?\.]+)?'
end
def to_s
'(.:format)?'
end
def extract_value
"#{local_name} = options[:#{key}] && options[:#{key}].to_s.downcase"
end
#the value should not include the period (.)
def match_extraction(next_capture)
%[
if (m = match[#{next_capture}])
params[:#{key}] = URI.unescape(m.from(1))
end
]
end
end
end
end

View file

@ -0,0 +1,181 @@
require 'rack/utils'
module ActionController
module Session
class AbstractStore
ENV_SESSION_KEY = 'rack.session'.freeze
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
SET_COOKIE = 'Set-Cookie'.freeze
class SessionHash < Hash
def initialize(by, env)
super()
@by = by
@env = env
@loaded = false
end
def session_id
ActiveSupport::Deprecation.warn(
"ActionController::Session::AbstractStore::SessionHash#session_id " +
"has been deprecated. Please use request.session_options[:id] instead.", caller)
@env[ENV_SESSION_OPTIONS_KEY][:id]
end
def [](key)
load! unless @loaded
super
end
def []=(key, value)
load! unless @loaded
super
end
def to_hash
h = {}.replace(self)
h.delete_if { |k,v| v.nil? }
h
end
def data
ActiveSupport::Deprecation.warn(
"ActionController::Session::AbstractStore::SessionHash#data " +
"has been deprecated. Please use #to_hash instead.", caller)
to_hash
end
def inspect
load! unless @loaded
super
end
private
def loaded?
@loaded
end
def load!
stale_session_check! do
id, session = @by.send(:load_session, @env)
(@env[ENV_SESSION_OPTIONS_KEY] ||= {})[:id] = id
replace(session)
@loaded = true
end
end
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
# Note that the regexp does not allow $1 to end with a ':'
$1.constantize
rescue LoadError, NameError => const_error
raise ActionController::SessionRestoreError, "Session contains objects whose class definition isn\\'t available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: \#{const_error.message} [\#{const_error.class}])\n"
end
retry
else
raise
end
end
end
DEFAULT_OPTIONS = {
:key => '_session_id',
:path => '/',
:domain => nil,
:expire_after => nil,
:secure => false,
:httponly => true,
:cookie_only => true
}
def initialize(app, options = {})
# Process legacy CGI options
options = options.symbolize_keys
if options.has_key?(:session_path)
options[:path] = options.delete(:session_path)
end
if options.has_key?(:session_key)
options[:key] = options.delete(:session_key)
end
if options.has_key?(:session_http_only)
options[:httponly] = options.delete(:session_http_only)
end
@app = app
@default_options = DEFAULT_OPTIONS.merge(options)
@key = @default_options[:key]
@cookie_only = @default_options[:cookie_only]
end
def call(env)
session = SessionHash.new(self, env)
env[ENV_SESSION_KEY] = session
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
response = @app.call(env)
session_data = env[ENV_SESSION_KEY]
options = env[ENV_SESSION_OPTIONS_KEY]
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
sid = options[:id] || generate_sid
unless set_session(env, sid, session_data.to_hash)
return response
end
cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid)
cookie << "; domain=#{options[:domain]}" if options[:domain]
cookie << "; path=#{options[:path]}" if options[:path]
if options[:expire_after]
expiry = Time.now + options[:expire_after]
cookie << "; expires=#{expiry.httpdate}"
end
cookie << "; Secure" if options[:secure]
cookie << "; HttpOnly" if options[:httponly]
headers = response[1]
unless headers[SET_COOKIE].blank?
headers[SET_COOKIE] << "\n#{cookie}"
else
headers[SET_COOKIE] = cookie
end
end
response
end
private
def generate_sid
ActiveSupport::SecureRandom.hex(16)
end
def load_session(env)
request = Rack::Request.new(env)
sid = request.cookies[@key]
unless @cookie_only
sid ||= request.params[@key]
end
sid, session = get_session(env, sid)
[sid, session]
end
def get_session(env, sid)
raise '#get_session needs to be implemented.'
end
def set_session(env, sid, session_data)
raise '#set_session needs to be implemented.'
end
end
end
end

View file

@ -1,340 +0,0 @@
require 'cgi'
require 'cgi/session'
require 'digest/md5'
class CGI
class Session
attr_reader :data
# Return this session's underlying Session instance. Useful for the DB-backed session stores.
def model
@dbman.model if @dbman
end
# A session store backed by an Active Record class. A default class is
# provided, but any object duck-typing to an Active Record Session class
# with text +session_id+ and +data+ attributes is sufficient.
#
# The default assumes a +sessions+ tables with columns:
# +id+ (numeric primary key),
# +session_id+ (text, or longtext if your session data exceeds 65K), and
# +data+ (text or longtext; careful if your session data exceeds 65KB).
# The +session_id+ column should always be indexed for speedy lookups.
# Session data is marshaled to the +data+ column in Base64 format.
# If the data you write is larger than the column's size limit,
# ActionController::SessionOverflowError will be raised.
#
# You may configure the table name, primary key, and data column.
# For example, at the end of <tt>config/environment.rb</tt>:
# CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
# CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
# CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
# Note that setting the primary key to the +session_id+ frees you from
# having a separate +id+ column if you don't want it. However, you must
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
# on ApplicationController is a good place.
#
# Since the default class is a simple Active Record, you get timestamps
# for free if you add +created_at+ and +updated_at+ datetime columns to
# the +sessions+ table, making periodic session expiration a snap.
#
# You may provide your own session class implementation, whether a
# feature-packed Active Record or a bare-metal high-performance SQL
# store, by setting
# CGI::Session::ActiveRecordStore.session_class = MySessionClass
# You must implement these methods:
# self.find_by_session_id(session_id)
# initialize(hash_of_session_id_and_data)
# attr_reader :session_id
# attr_accessor :data
# save
# destroy
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
class ActiveRecordStore
# The default Active Record class.
class Session < ActiveRecord::Base
# Customizable data column name. Defaults to 'data'.
cattr_accessor :data_column_name
self.data_column_name = 'data'
before_save :marshal_data!
before_save :raise_on_session_data_overflow!
class << self
# Don't try to reload ARStore::Session in dev mode.
def reloadable? #:nodoc:
false
end
def data_column_size_limit
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
end
# Hook to set up sessid compatibility.
def find_by_session_id(session_id)
setup_sessid_compatibility!
find_by_session_id(session_id)
end
def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
def create_table!
connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
)
end_sql
end
def drop_table!
connection.execute "DROP TABLE #{table_name}"
end
private
# Compatibility with tables using sessid instead of session_id.
def setup_sessid_compatibility!
# Reset column info since it may be stale.
reset_column_information
if columns_hash['sessid']
def self.find_by_session_id(*args)
find_by_sessid(*args)
end
define_method(:session_id) { sessid }
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
end
end
end
end
# Lazy-unmarshal session state.
def data
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
end
attr_writer :data
# Has the session been loaded yet?
def loaded?
!! @data
end
private
def marshal_data!
return false if !loaded?
write_attribute(@@data_column_name, self.class.marshal(self.data))
end
# Ensures that the data about to be stored in the database is not
# larger than the data storage column. Raises
# ActionController::SessionOverflowError.
def raise_on_session_data_overflow!
return false if !loaded?
limit = self.class.data_column_size_limit
if loaded? and limit and read_attribute(@@data_column_name).size > limit
raise ActionController::SessionOverflowError
end
end
end
# A barebones session store which duck-types with the default session
# store but bypasses Active Record and issues SQL directly. This is
# an example session model class meant as a basis for your own classes.
#
# The database connection, table name, and session id and data columns
# are configurable class attributes. Marshaling and unmarshaling
# are implemented as class methods that you may override. By default,
# marshaling data is
#
# ActiveSupport::Base64.encode64(Marshal.dump(data))
#
# and unmarshaling data is
#
# Marshal.load(ActiveSupport::Base64.decode64(data))
#
# This marshaling behavior is intended to store the widest range of
# binary session data in a +text+ column. For higher performance,
# store in a +blob+ column instead and forgo the Base64 encoding.
class SqlBypass
# Use the ActiveRecord::Base.connection by default.
cattr_accessor :connection
# The table name defaults to 'sessions'.
cattr_accessor :table_name
@@table_name = 'sessions'
# The session id field defaults to 'session_id'.
cattr_accessor :session_id_column
@@session_id_column = 'session_id'
# The data field defaults to 'data'.
cattr_accessor :data_column
@@data_column = 'data'
class << self
def connection
@@connection ||= ActiveRecord::Base.connection
end
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
def create_table!
@@connection.execute <<-end_sql
CREATE TABLE #{table_name} (
id INTEGER PRIMARY KEY,
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
#{@@connection.quote_column_name(data_column)} TEXT
)
end_sql
end
def drop_table!
@@connection.execute "DROP TABLE #{table_name}"
end
end
attr_reader :session_id
attr_writer :data
# Look for normal and marshaled data, self.find_by_session_id's way of
# telling us to postpone unmarshaling until the data is requested.
# We need to handle a normal data attribute in case of a new record.
def initialize(attributes)
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
@new_record = @marshaled_data.nil?
end
def new_record?
@new_record
end
# Lazy-unmarshal session state.
def data
unless @data
if @marshaled_data
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
else
@data = {}
end
end
@data
end
def loaded?
!! @data
end
def save
return false if !loaded?
marshaled_data = self.class.marshal(data)
if @new_record
@new_record = false
@@connection.update <<-end_sql, 'Create session'
INSERT INTO #{@@table_name} (
#{@@connection.quote_column_name(@@session_id_column)},
#{@@connection.quote_column_name(@@data_column)} )
VALUES (
#{@@connection.quote(session_id)},
#{@@connection.quote(marshaled_data)} )
end_sql
else
@@connection.update <<-end_sql, 'Update session'
UPDATE #{@@table_name}
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
def destroy
unless @new_record
@@connection.delete <<-end_sql, 'Destroy session'
DELETE FROM #{@@table_name}
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
end_sql
end
end
end
# The class used for session storage. Defaults to
# CGI::Session::ActiveRecordStore::Session.
cattr_accessor :session_class
self.session_class = Session
# Find or instantiate a session given a CGI::Session.
def initialize(session, option = nil)
session_id = session.session_id
unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
unless session.new_session
raise CGI::Session::NoSession, 'uninitialized session'
end
@session = @@session_class.new(:session_id => session_id, :data => {})
# session saving can be lazy again, because of improved component implementation
# therefore next line gets commented out:
# @session.save
end
end
# Access the underlying session model.
def model
@session
end
# Restore session state. The session model handles unmarshaling.
def restore
if @session
@session.data
end
end
# Save session store.
def update
if @session
ActiveRecord::Base.silence { @session.save }
end
end
# Save and close the session store.
def close
if @session
update
@session = nil
end
end
# Delete and close the session store.
def delete
if @session
ActiveRecord::Base.silence { @session.destroy }
@session = nil
end
end
protected
def logger
ActionController::Base.logger rescue nil
end
end
end
end

View file

@ -1,167 +1,221 @@
require 'cgi'
require 'cgi/session'
require 'openssl' # to generate the HMAC message digest
module ActionController
module Session
# 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.
#
# 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:
#
# * <tt>:secret</tt>: 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. Examples:
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: 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 CookieStore
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
# 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:
#
# * <tt>:secret</tt>: 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. Examples:
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: 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
DEFAULT_OPTIONS = {
:key => '_session_id',
:domain => nil,
:path => "/",
:expire_after => nil,
:httponly => true
}.freeze
# Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end
ENV_SESSION_KEY = "rack.session".freeze
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
HTTP_SET_COOKIE = "Set-Cookie".freeze
# Raised when the cookie fails its integrity check.
class TamperedWithCookie < StandardError; end
# Raised when storing more than 4K of session data.
class CookieOverflow < 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'],
'http_only' => options['session_http_only']
}
# 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' => nil, '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 = ActiveSupport::Base64.encode64s(Marshal.dump(session))
"#{data}--#{generate_digest(data)}"
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
if cookie
data, digest = cookie.split('--')
# Do two checks to transparently support old double-escaped data.
unless digest == generate_digest(data) || digest == generate_digest(data = CGI.unescape(data))
delete
raise TamperedWithCookie
def initialize(app, options = {})
# Process legacy CGI options
options = options.symbolize_keys
if options.has_key?(:session_path)
options[:path] = options.delete(:session_path)
end
if options.has_key?(:session_key)
options[:key] = options.delete(:session_key)
end
if options.has_key?(:session_http_only)
options[:httponly] = options.delete(:session_http_only)
end
Marshal.load(ActiveSupport::Base64.decode64(data))
@app = app
# The session_key option is required.
ensure_session_key(options[:key])
@key = options.delete(:key).freeze
# The secret option is required.
ensure_secret_secure(options[:secret])
@secret = options.delete(:secret).freeze
@digest = options.delete(:digest) || 'SHA1'
@verifier = verifier_for(@secret, @digest)
@default_options = DEFAULT_OPTIONS.merge(options).freeze
freeze
end
end
# Read the session data cookie.
def read_cookie
@session.cgi.cookies[@cookie_options['name']].first
end
def call(env)
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
# 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
status, headers, body = @app.call(env)
# Clear cookie value so subsequent new_session doesn't reload old data.
def clear_old_cookie_value
@session.cgi.cookies[@cookie_options['name']].clear
session_data = env[ENV_SESSION_KEY]
options = env[ENV_SESSION_OPTIONS_KEY]
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
session_data = marshal(session_data.to_hash)
raise CookieOverflow if session_data.size > MAX
cookie = Hash.new
cookie[:value] = session_data
unless options[:expire_after].nil?
cookie[:expires] = Time.now + options[:expire_after]
end
cookie = build_cookie(@key, cookie.merge(options))
unless headers[HTTP_SET_COOKIE].blank?
headers[HTTP_SET_COOKIE] << "\n#{cookie}"
else
headers[HTTP_SET_COOKIE] = cookie
end
end
[status, headers, body]
end
private
# Should be in Rack::Utils soon
def build_cookie(key, value)
case value
when Hash
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
# According to RFC 2109, we need dashes here.
# N.B.: cgi.rb uses spaces...
expires = "; expires=" + value[:expires].clone.gmtime.
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
secure = "; secure" if value[:secure]
httponly = "; HttpOnly" if value[:httponly]
value = value[:value]
end
value = [value] unless Array === value
cookie = Rack::Utils.escape(key) + "=" +
value.map { |v| Rack::Utils.escape(v) }.join("&") +
"#{domain}#{path}#{expires}#{secure}#{httponly}"
end
def load_session(env)
request = Rack::Request.new(env)
session_data = request.cookies[@key]
data = unmarshal(session_data) || persistent_session_id!({})
[data[:session_id], data]
end
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
@verifier.generate(persistent_session_id!(session))
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
persistent_session_id!(@verifier.verify(cookie)) if cookie
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
def ensure_session_key(key)
if key.blank?
raise ArgumentError, 'A key is required to write a ' +
'cookie containing the session data. Use ' +
'config.action_controller.session = { :key => ' +
'"_myapp_session", :secret => "some secret phrase" } in ' +
'config/environment.rb'
end
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, "A secret is required to generate an " +
"integrity hash for cookie session data. Use " +
"config.action_controller.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, "Secret should be something secure, " +
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
"provided, \"#{secret}\", is shorter than the minimum length " +
"of #{SECRET_MIN_LENGTH} characters"
end
end
def verifier_for(secret, digest)
key = secret.respond_to?(:call) ? secret.call : secret
ActiveSupport::MessageVerifier.new(key, digest)
end
def generate_sid
ActiveSupport::SecureRandom.hex(16)
end
def persistent_session_id!(data)
(data ||= {}).merge!(inject_persistent_session_id(data))
end
def inject_persistent_session_id(data)
requires_session_id?(data) ? { :session_id => generate_sid } : {}
end
def requires_session_id?(data)
if data
data.respond_to?(:key?) && !data.key?(:session_id)
else
true
end
end
end
end
end

View file

@ -1,32 +0,0 @@
#!/usr/bin/env ruby
# This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access.
require 'drb'
session_hash = Hash.new
session_hash.instance_eval { @mutex = Mutex.new }
class <<session_hash
def []=(key, value)
@mutex.synchronize do
super(key, value)
end
end
def [](key)
@mutex.synchronize do
super(key)
end
end
def delete(key)
@mutex.synchronize do
super(key)
end
end
end
DRb.start_service('druby://127.0.0.1:9192', session_hash)
DRb.thread.join

View file

@ -1,35 +0,0 @@
require 'cgi'
require 'cgi/session'
require 'drb'
class CGI #:nodoc:all
class Session
class DRbStore
@@session_data = DRbObject.new(nil, 'druby://localhost:9192')
def initialize(session, option=nil)
@session_id = session.session_id
end
def restore
@h = @@session_data[@session_id] || {}
end
def update
@@session_data[@session_id] = @h
end
def close
update
end
def delete
@@session_data.delete(@session_id)
end
def data
@@session_data[@session_id]
end
end
end
end

View file

@ -1,95 +1,48 @@
# cgi/session/memcached.rb - persistent storage of marshalled session data
#
# == Overview
#
# This file provides the CGI::Session::MemCache class, which builds
# persistence of storage data on top of the MemCache library. See
# cgi/session.rb for more details on session storage managers.
#
begin
require 'cgi/session'
require_library_or_gem 'memcache'
class CGI
class Session
# MemCache-based session storage class.
#
# This builds upon the top-level MemCache class provided by the
# library file memcache.rb. Session data is marshalled and stored
# in a memcached cache.
class MemCacheStore
def check_id(id) #:nodoc:#
/[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
end
module ActionController
module Session
class MemCacheStore < AbstractStore
def initialize(app, options = {})
# Support old :expires option
options[:expire_after] ||= options[:expires]
# Create a new CGI::Session::MemCache instance
#
# This constructor is used internally by CGI::Session. The
# user does not generally need to call it directly.
#
# +session+ is the session for which this instance is being
# created. The session id must only contain alphanumeric
# characters; automatically generated session ids observe
# this requirement.
#
# +options+ is a hash of options for the initializer. The
# following options are recognized:
#
# cache:: an instance of a MemCache client to use as the
# session cache.
#
# expires:: an expiry time value to use for session entries in
# the session cache. +expires+ is interpreted in seconds
# relative to the current time if its less than 60*60*24*30
# (30 days), or as an absolute Unix time (e.g., Time#to_i) if
# greater. If +expires+ is +0+, or not passed on +options+,
# the entry will never expire.
#
# This session's memcache entry will be created if it does
# not exist, or retrieved if it does.
def initialize(session, options = {})
id = session.session_id
unless check_id(id)
raise ArgumentError, "session_id '%s' is invalid" % id
super
@default_options = {
:namespace => 'rack:session',
:memcache_server => 'localhost:11211'
}.merge(@default_options)
@pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
unless @pool.servers.any? { |s| s.alive? }
raise "#{self} unable to find server during initialization."
end
@cache = options['cache'] || MemCache.new('localhost')
@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)
@mutex = Mutex.new
super
end
private
def get_session(env, sid)
sid ||= generate_sid
begin
session = @pool.get(sid) || {}
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
session = {}
end
[sid, session]
end
end
# Restore session state from the session's memcache entry.
#
# Returns the session state as a hash.
def restore
@session_data = @cache[@session_key] || {}
end
# Save session state to the session's memcache entry.
def update
@cache.set(@session_key, @session_data, @expires)
end
# Update and close the session's memcache entry.
def close
update
end
# Delete the session's memcache entry.
def delete
@cache.delete(@session_key)
@session_data = {}
end
def data
@session_data
end
def set_session(env, sid, session_data)
options = env['rack.session.options']
expiry = options[:expire_after] || 0
@pool.set(sid, session_data, expiry)
return true
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
return false
end
end
end
end

View file

@ -1,17 +1,8 @@
require 'action_controller/session/cookie_store'
require 'action_controller/session/drb_store'
require 'action_controller/session/mem_cache_store'
if Object.const_defined?(:ActiveRecord)
require 'action_controller/session/active_record_store'
end
module ActionController #:nodoc:
module SessionManagement #:nodoc:
def self.included(base)
base.class_eval do
extend ClassMethods
alias_method_chain :process, :session_management_support
alias_method_chain :process_cleanup, :session_management_support
end
end
@ -19,144 +10,45 @@ module ActionController #:nodoc:
# Set the session store to be used for keeping the session data between requests.
# By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
# but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
# <tt>:p_store</tt>, <tt>:drb_store</tt>, <tt>:mem_cache_store</tt>, or
# <tt>:memory_store</tt>) or your own custom class.
# <tt>:mem_cache_store</tt>, 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
if store == :active_record_store
self.session_store = ActiveRecord::SessionStore
else
@@session_store = store.is_a?(Symbol) ?
Session.const_get(store.to_s.camelize) :
store
end
end
# Returns the session store class currently used.
def session_store
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager]
if defined? @@session_store
@@session_store
else
Session::CookieStore
end
end
def session=(options = {})
self.session_store = nil if options.delete(:disabled)
session_options.merge!(options)
end
# Returns the hash used to configure the session. Example use:
#
# ActionController::Base.session_options[:session_secure] = true # session only available over HTTPS
# ActionController::Base.session_options[:secure] = true # session only available over HTTPS
def session_options
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
@session_options ||= {}
end
# Specify how sessions ought to be managed for a subset of the actions on
# the controller. Like filters, you can specify <tt>:only</tt> and
# <tt>:except</tt> clauses to restrict the subset, otherwise options
# apply to all actions on this controller.
#
# The session options are inheritable, as well, so if you specify them in
# a parent controller, they apply to controllers that extend the parent.
#
# Usage:
#
# # turn off session management for all actions.
# session :off
#
# # turn off session management for all actions _except_ foo and bar.
# session :off, :except => %w(foo bar)
#
# # turn off session management for only the foo and bar actions.
# session :off, :only => %w(foo bar)
#
# # the session will only work over HTTPS, but only for the foo action
# session :only => :foo, :session_secure => true
#
# # the session by default uses HttpOnly sessions for security reasons.
# # this can be switched off.
# session :only => :foo, :session_http_only => false
#
# # the session will only be disabled for 'foo', and only if it is
# # requested as a web service
# 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?) }
#
# # turn the session back on, useful when it was turned off in the
# # application controller, and you need it on in another controller
# session :on
#
# All session options described for ActionController::Base.process_cgi
# are valid arguments.
def session(*args)
options = args.extract_options!
options[:disabled] = false if args.delete(:on)
options[:disabled] = true if !args.empty?
options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only]
options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except]
if options[:only] && options[:except]
raise ArgumentError, "only one of either :only or :except are allowed"
end
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
def session_options_for(request, action) #:nodoc:
if (session_options = cached_session_options).empty?
{}
else
options = {}
action = action.to_s
session_options.each do |opts|
next if opts[:if] && !opts[:if].call(request)
if opts[:only] && opts[:only].include?(action)
options.merge!(opts)
elsif opts[:except] && !opts[:except].include?(action)
options.merge!(opts)
elsif !opts[:only] && !opts[:except]
options.merge!(opts)
end
end
if options.empty? then options
else
options.delete :only
options.delete :except
options.delete :if
options[:disabled] ? false : options
end
end
ActiveSupport::Deprecation.warn(
"Disabling sessions for a single controller has been deprecated. " +
"Sessions are now lazy loaded. So if you don't access them, " +
"consider them off. You can still modify the session cookie " +
"options with request.session_options.", caller)
end
end
def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc:
set_session_options(request)
process_without_session_management_support(request, response, method, *arguments)
end
private
def set_session_options(request)
request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index")
end
def process_cleanup_with_session_management_support
clear_persistent_model_associations
process_cleanup_without_session_management_support
end
# Clear cached associations in session data so they don't overflow
# the database field. Only applies to ActiveRecordStore since there
# is not a standard way to iterate over session data.
def clear_persistent_model_associations #:doc:
if defined?(@_session) && @_session.respond_to?(:data)
session_data = @_session.data
if session_data && session_data.respond_to?(:each_value)
session_data.each_value do |obj|
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
end
end
end
end
end
end

View file

@ -1,5 +1,8 @@
require 'active_support/core_ext/string/bytesize'
module ActionController #:nodoc:
# Methods for sending files and streams to the browser instead of rendering.
# Methods for sending arbitrary data and for streaming files to the browser,
# instead of rendering.
module Streaming
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze,
@ -24,7 +27,8 @@ module ActionController #:nodoc:
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
# either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
# * <tt>:length</tt> - used to manually override the length (in bytes) of the content that
# is going to be sent to the client. Defaults to <tt>File.size(path)</tt>.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
@ -102,12 +106,16 @@ module ActionController #:nodoc:
end
end
# Send binary data to the user as a file download. May set content type, apparent file name,
# and specify whether to show data inline or download as an attachment.
# Sends the given binary data to the browser. This method is similar to
# <tt>render :text => data</tt>, but also allows you to specify whether
# the browser should display the response as a file attachment (i.e. in a
# download dialog) or as inline data. You may also set the content type,
# the apparent file name, and other things.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
# either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
@ -125,9 +133,13 @@ module ActionController #:nodoc:
# send_data image.data, :type => image.content_type, :disposition => 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
#
# <b>Tip:</b> if you want to stream large amounts of on-the-fly generated
# data to the browser, then use <tt>render :text => proc { ... }</tt>
# instead. See ActionController::Base#render for more information.
def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" if logger
send_file_headers! options.merge(:length => data.size)
send_file_headers! options.merge(:length => data.bytesize)
@performed_render = false
render :status => options[:status], :text => data
end
@ -143,9 +155,16 @@ module ActionController #:nodoc:
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
headers.update(
'Content-Length' => options[:length],
'Content-Type' => options[:type].to_s.strip, # fixes a problem with extra '\r' with some browsers
content_type = options[:type]
if content_type.is_a?(Symbol)
raise ArgumentError, "Unknown MIME type #{options[:type]}" unless Mime::EXTENSION_LOOKUP.has_key?(content_type.to_s)
content_type = Mime::Type.lookup_by_extension(content_type.to_s)
end
content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers
headers.merge!(
'Content-Length' => options[:length].to_s,
'Content-Type' => content_type,
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)

View file

@ -0,0 +1,29 @@
module ActionController
class StringCoercion
class UglyBody < ActiveSupport::BasicObject
def initialize(body)
@body = body
end
def each
@body.each do |part|
yield part.to_s
end
end
private
def method_missing(*args, &block)
@body.__send__(*args, &block)
end
end
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
[status, headers, UglyBody.new(body)]
end
end
end

View file

@ -6,6 +6,6 @@
</h1>
<pre><%=h @exception.clean_message %></pre>
<%= render(:file => @rescues_path + "/_trace.erb") %>
<%= render :file => @rescues_path["rescues/_trace.erb"] %>
<%= render(:file => @rescues_path + "/_request_and_response.erb") %>
<%= render :file => @rescues_path["rescues/_request_and_response.erb"] %>

View file

@ -15,7 +15,7 @@
<% @real_exception = @exception
@exception = @exception.original_exception || @exception %>
<%= render(:file => @rescues_path + "/_trace.erb") %>
<%= render :file => @rescues_path["rescues/_trace.erb"] %>
<% @exception = @real_exception %>
<%= render(:file => @rescues_path + "/_request_and_response.erb") %>
<%= render :file => @rescues_path["rescues/_request_and_response.erb"] %>

View file

@ -1,20 +1,7 @@
require 'active_support/test_case'
require 'action_controller/test_process'
module ActionController
class NonInferrableControllerError < ActionControllerError
def initialize(name)
@name = 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. This could mean that #{inferred_controller_name} does not exist " +
"or it contains syntax errors"
end
def inferred_controller_name
@name.sub(/Test$/, '')
end
end
# Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method. This should not be confused with
# integration tests (see ActionController::IntegrationTest), which are more like
@ -74,7 +61,70 @@ module ActionController
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController
# end
#
# == Testing controller internals
#
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * 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
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# 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 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.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
# @request.cookies["key"] = "value"
#
# == 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:
#
# assert_redirected_to page_url(:title => 'foo')
class TestCase < ActiveSupport::TestCase
include TestProcess
def initialize(*args)
super
@controller = nil
end
module Assertions
%w(response selector tag dom routing model).each do |kind|
include ActionController::Assertions.const_get("#{kind.camelize}Assertions")
end
def clean_backtrace(&block)
yield
rescue ActiveSupport::TestCase::Assertion => error
framework_path = Regexp.new(File.expand_path("#{File.dirname(__FILE__)}/assertions"))
error.backtrace.reject! { |line| File.expand_path(line) =~ framework_path }
raise
end
end
include Assertions
# When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
# (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
# rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
@ -82,17 +132,23 @@ module ActionController
#
# The exception is stored in the exception accessor for further inspection.
module RaiseActionExceptions
attr_accessor :exception
def rescue_action_without_handler(e)
self.exception = e
if request.remote_addr == "0.0.0.0"
raise(e)
else
super(e)
def self.included(base)
base.class_eval do
attr_accessor :exception
protected :exception, :exception=
end
end
protected
def rescue_action_without_handler(e)
self.exception = e
if request.remote_addr == "0.0.0.0"
raise(e)
else
super(e)
end
end
end
setup :setup_controller_request_and_response
@ -107,7 +163,7 @@ module ActionController
end
def controller_class=(new_class)
prepare_controller_class(new_class)
prepare_controller_class(new_class) if new_class
write_inheritable_attribute(:controller_class, new_class)
end
@ -122,7 +178,7 @@ module ActionController
def determine_default_controller_class(name)
name.sub(/Test$/, '').constantize
rescue NameError
raise NonInferrableControllerError.new(name)
nil
end
def prepare_controller_class(new_class)
@ -131,17 +187,23 @@ module ActionController
end
def setup_controller_request_and_response
@controller = self.class.controller_class.new
@controller.request = @request = TestRequest.new
@request = TestRequest.new
@response = TestResponse.new
@controller.params = {}
@controller.send(:initialize_current_url)
if klass = self.class.controller_class
@controller ||= klass.new rescue nil
end
if @controller
@controller.request = @request
@controller.params = {}
@controller.send(:initialize_current_url)
end
end
# Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local
def rescue_action_in_public!
@request.remote_addr = '208.77.188.166' # example.com
end
end
end
end

View file

@ -1,51 +1,28 @@
require 'action_controller/assertions'
require 'action_controller/test_case'
require 'rack/session/abstract/id'
module ActionController #:nodoc:
class Base
attr_reader :assigns
# Process a test request called with a TestRequest object.
def self.process_test(request)
new.process_test(request)
end
def process_test(request) #:nodoc:
process(request, TestResponse.new)
end
def process_with_test(*args)
returning process_without_test(*args) do
@assigns = {}
(instance_variable_names - @@protected_instance_variables).each do |var|
value = instance_variable_get(var)
@assigns[var[1..-1]] = value
response.template.assigns[var[1..-1]] = value if response
end
end
end
alias_method_chain :process, :test
end
class TestRequest < AbstractRequest #:nodoc:
class TestRequest < Request #:nodoc:
attr_accessor :cookies, :session_options
attr_accessor :query_parameters, :request_parameters, :path, :session
attr_accessor :host, :user_agent
attr_accessor :query_parameters, :path, :session
attr_accessor :host
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@query_parameters = query_parameters || {}
@request_parameters = request_parameters || {}
@session = session || TestSession.new
def self.new(env = {})
super
end
def initialize(env = {})
super(Rack::MockRequest.env_for("/").merge(env))
@query_parameters = {}
@session = TestSession.new
default_rack_options = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
@session_options ||= {:id => generate_sid(default_rack_options[:sidbits])}.merge(default_rack_options)
initialize_containers
initialize_default_values
super()
initialize_containers
end
def reset_session
@session = TestSession.new
@session.reset
end
# Wraps raw_post in a StringIO.
@ -56,12 +33,15 @@ module ActionController #:nodoc:
# Either the RAW_POST_DATA environment variable or the URL-encoded request
# parameters.
def raw_post
env['RAW_POST_DATA'] ||= returning(url_encoded_request_parameters) { |b| b.force_encoding(Encoding::BINARY) if b.respond_to?(:force_encoding) }
@env['RAW_POST_DATA'] ||= begin
data = url_encoded_request_parameters
data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding)
data
end
end
def port=(number)
@env["SERVER_PORT"] = number.to_i
port(true)
end
def action=(action_name)
@ -75,8 +55,6 @@ module ActionController #:nodoc:
@env["REQUEST_URI"] = value
@request_uri = nil
@path = nil
request_uri(true)
path(true)
end
def request_uri=(uri)
@ -84,9 +62,13 @@ module ActionController #:nodoc:
@path = uri.split("?").first
end
def request_method=(method)
@request_method = method
end
def accept=(mime_types)
@env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
accepts(true)
@accepts = nil
end
def if_modified_since=(last_modified)
@ -102,14 +84,14 @@ module ActionController #:nodoc:
end
def request_uri(*args)
@request_uri || super
@request_uri || super()
end
def path(*args)
@path || super
@path || super()
end
def assign_parameters(controller_path, action, parameters)
def assign_parameters(controller_path, action, parameters = {})
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
non_path_parameters = get? ? query_parameters : request_parameters
@ -126,26 +108,35 @@ module ActionController #:nodoc:
path_parameters[key.to_s] = value
end
end
raw_post # populate env['RAW_POST_DATA']
@parameters = nil # reset TestRequest#parameters to use the new path_parameters
end
def recycle!
self.request_parameters = {}
@env["action_controller.request.request_parameters"] = {}
self.query_parameters = {}
self.path_parameters = {}
unmemoize_all
@headers, @request_method, @accepts, @content_type = nil, nil, nil, nil
end
def user_agent=(user_agent)
@env['HTTP_USER_AGENT'] = user_agent
end
private
def generate_sid(sidbits)
"%0#{sidbits / 4}x" % rand(2**sidbits - 1)
end
def initialize_containers
@env, @cookies = {}, {}
@cookies = {}
end
def initialize_default_values
@host = "test.host"
@request_uri = "/"
@user_agent = "Rails Testing"
self.remote_addr = "0.0.0.0"
@env['HTTP_USER_AGENT'] = "Rails Testing"
@env['REMOTE_ADDR'] = "0.0.0.0"
@env["SERVER_PORT"] = 80
@env['REQUEST_METHOD'] = "GET"
end
@ -167,7 +158,7 @@ module ActionController #:nodoc:
module TestResponseBehavior #:nodoc:
# The response code of the request
def response_code
status[0,3].to_i rescue 0
status.to_s[0,3].to_i rescue 0
end
# Returns a String to ensure compatibility with Net::HTTPResponse
@ -201,6 +192,11 @@ module ActionController #:nodoc:
alias_method :server_error?, :error?
# Was there a client client?
def client_error?
(400..499).include?(response_code)
end
# Returns the redirection location or nil
def redirect_url
headers['Location']
@ -217,8 +213,8 @@ module ActionController #:nodoc:
# Returns the template of the file which was used to
# render this response (or nil)
def rendered_template
template.instance_variable_get(:@_first_render)
def rendered
template.instance_variable_get(:@_rendered)
end
# A shortcut to the flash. Returns an empty hash if no session flash exists.
@ -228,7 +224,7 @@ module ActionController #:nodoc:
# Do we have a flash?
def has_flash?
!session['flash'].empty?
!flash.empty?
end
# Do we have a flash that has contents?
@ -256,11 +252,16 @@ module ActionController #:nodoc:
!template_objects[name].nil?
end
# Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs
# Returns the response cookies, converted to a Hash of (name => value) pairs
#
# assert_equal ['AuthorOfNewPage'], r.cookies['author'].value
# assert_equal 'AuthorOfNewPage', r.cookies['author']
def cookies
headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash }
cookies = {}
Array(headers['Set-Cookie']).each do |cookie|
key, value = cookie.split(";").first.split("=").map {|val| Rack::Utils.unescape(val)}
cookies[key] = value
end
cookies
end
# Returns binary content (downloadable file), converted to a String
@ -281,48 +282,72 @@ module ActionController #:nodoc:
# TestResponse, which represent the HTTP response results of the requested
# controller actions.
#
# See AbstractResponse for more information on controller response objects.
class TestResponse < AbstractResponse
# See Response for more information on controller response objects.
class TestResponse < Response
include TestResponseBehavior
def recycle!
headers.delete('ETag')
headers.delete('Last-Modified')
end
end
class TestSession #:nodoc:
class TestSession < Hash #:nodoc:
attr_accessor :session_id
def initialize(attributes = nil)
@session_id = ''
@attributes = attributes.nil? ? nil : attributes.stringify_keys
@saved_attributes = nil
reset_session_id
replace_attributes(attributes)
end
def reset
reset_session_id
replace_attributes({ })
end
def data
@attributes ||= @saved_attributes || {}
to_hash
end
def [](key)
data[key.to_s]
super(key.to_s)
end
def []=(key, value)
data[key.to_s] = value
super(key.to_s, value)
end
def update
@saved_attributes = @attributes
def update(hash = nil)
if hash.nil?
ActiveSupport::Deprecation.warn('use replace instead', caller)
replace({})
else
super(hash)
end
end
def delete
@attributes = nil
def delete(key = nil)
if key.nil?
ActiveSupport::Deprecation.warn('use clear instead', caller)
clear
else
super(key.to_s)
end
end
def close
update
delete
ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller)
end
private
def reset_session_id
@session_id = ''
end
def replace_attributes(attributes = nil)
attributes ||= {}
replace(attributes.stringify_keys)
end
end
@ -333,10 +358,10 @@ module ActionController #:nodoc:
# a file upload.
#
# Usage example, within a functional test:
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::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)
# post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary)
require 'tempfile'
class TestUploadedFile
# The filename, *not* including the path, of the "uploaded" file
@ -368,20 +393,33 @@ module ActionController #:nodoc:
module TestProcess
def self.included(base)
# execute the request simulating a specific HTTP method and set/volley the response
# TODO: this should be un-DRY'ed for the sake of API documentation.
%w( get post put delete head ).each do |method|
base.class_eval <<-EOV, __FILE__, __LINE__
def #{method}(action, parameters = nil, session = nil, flash = nil)
@request.env['REQUEST_METHOD'] = "#{method.upcase}" if defined?(@request)
process(action, parameters, session, flash)
end
EOV
# Executes a request simulating GET HTTP method and set/volley the response
def get(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "GET")
end
# Executes a request simulating POST HTTP method and set/volley the response
def post(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "POST")
end
# Executes a request simulating PUT HTTP method and set/volley the response
def put(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "PUT")
end
# Executes a request simulating DELETE HTTP method and set/volley the response
def delete(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "DELETE")
end
# Executes a request simulating HEAD HTTP method and set/volley the response
def head(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "HEAD")
end
end
# execute the request and set/volley the response
def process(action, parameters = nil, session = nil, flash = nil)
def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
# Sanity check for required instance variables so we can give an
# understandable error message.
%w(@controller @request @response).each do |iv_name|
@ -394,7 +432,7 @@ module ActionController #:nodoc:
@response.recycle!
@html_document = nil
@request.env['REQUEST_METHOD'] ||= "GET"
@request.env['REQUEST_METHOD'] = http_method
@request.action = action.to_s
@ -404,12 +442,14 @@ module ActionController #:nodoc:
@request.session = ActionController::TestSession.new(session) unless session.nil?
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
build_request_uri(action, parameters)
@controller.process(@request, @response)
Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest
@controller.process_with_test(@request, @response)
end
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, */*'
@request.env['HTTP_ACCEPT'] = [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
returning __send__(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
@ -426,7 +466,7 @@ module ActionController #:nodoc:
end
def session
@response.session
@request.session
end
def flash
@ -464,15 +504,15 @@ module ActionController #:nodoc:
html_document.find_all(conditions)
end
def method_missing(selector, *args)
if ActionController::Routing::Routes.named_routes.helpers.include?(selector)
@controller.send(selector, *args)
def method_missing(selector, *args, &block)
if @controller && ActionController::Routing::Routes.named_routes.helpers.include?(selector)
@controller.send(selector, *args, &block)
else
super
end
end
# Shortcut for <tt>ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type)</tt>:
# Shortcut for <tt>ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>:
#
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
#
@ -481,11 +521,8 @@ module ActionController #:nodoc:
#
# 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,
binary
)
fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
end
# A helper to make it easier to test different route configurations.
@ -520,12 +557,24 @@ module ActionController #:nodoc:
ActionController::Routing.const_set(:Routes, real_routes) if real_routes
end
end
end
module Test
module Unit
class TestCase #:nodoc:
include ActionController::TestProcess
module ProcessWithTest #:nodoc:
def self.included(base)
base.class_eval { attr_reader :assigns }
end
def process_with_test(*args)
process(*args).tap { set_test_assigns }
end
private
def set_test_assigns
@assigns = {}
(instance_variable_names - self.class.protected_instance_variables).each do |var|
name, value = var[1..-1], instance_variable_get(var)
@assigns[name] = value
response.template.assigns[name] = value if response
end
end
end
end

View file

@ -1,12 +1,12 @@
module ActionController
module Translation
def translate(*args)
I18n.translate *args
I18n.translate(*args)
end
alias :t :translate
def localize(*args)
I18n.localize *args
I18n.localize(*args)
end
alias :l :localize
end

View file

@ -0,0 +1,44 @@
module ActionController
module UploadedFile
def self.included(base)
base.class_eval do
attr_accessor :original_path, :content_type
alias_method :local_path, :path if method_defined?(:path)
end
end
def self.extended(object)
object.class_eval do
attr_accessor :original_path, :content_type
alias_method :local_path, :path if method_defined?(:path)
end
end
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
# The Windows regexp is adapted from Perl's File::Basename.
def original_filename
unless defined? @original_filename
@original_filename =
unless original_path.blank?
if original_path =~ /^(?:.*[:\\\/])?(.*)/m
$1
else
File.basename original_path
end
end
end
@original_filename
end
end
class UploadedStringIO < StringIO
include UploadedFile
end
class UploadedTempfile < Tempfile
include UploadedFile
end
end

View file

@ -92,15 +92,12 @@ module ActionController
# end
# end
module UrlWriter
# The default options for urls written by this writer. Typically a <tt>:host</tt>
# pair is provided.
mattr_accessor :default_url_options
self.default_url_options = {}
def self.included(base) #:nodoc:
ActionController::Routing::Routes.install_helpers(base)
base.mattr_accessor :default_url_options
base.default_url_options ||= default_url_options
# The default options for urls written by this writer. Typically a <tt>:host</tt> pair is provided.
base.default_url_options ||= {}
end
# Generate a url based on the options provided, default_url_options and the
@ -187,7 +184,7 @@ module ActionController
path = rewrite_path(options)
rewritten_url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
rewritten_url << "##{options[:anchor]}" if options[:anchor]
rewritten_url << "##{CGI.escape(options[:anchor].to_param.to_s)}" if options[:anchor]
rewritten_url
end

View file

@ -0,0 +1,16 @@
$LOAD_PATH << "#{File.dirname(__FILE__)}/html-scanner"
module HTML
autoload :CDATA, 'html/node'
autoload :Document, 'html/document'
autoload :FullSanitizer, 'html/sanitizer'
autoload :LinkSanitizer, 'html/sanitizer'
autoload :Node, 'html/node'
autoload :Sanitizer, 'html/sanitizer'
autoload :Selector, 'html/selector'
autoload :Tag, 'html/node'
autoload :Text, 'html/node'
autoload :Tokenizer, 'html/tokenizer'
autoload :Version, 'html/version'
autoload :WhiteListSanitizer, 'html/sanitizer'
end

View file

@ -162,7 +162,7 @@ module HTML #:nodoc:
end
closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[-:\w\x00-\x09\x0b-\x0c\x0e-\x1f]+/)
name.downcase!
unless closing

View file

@ -556,7 +556,7 @@ module HTML
end
# Attribute value.
next if statement.sub!(/^\[\s*([[:alpha:]][\w\-]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
name, equality, value = $1, $2, $3
if value == "?"
value = values.shift

View file

@ -90,7 +90,7 @@ module ActionController #:nodoc:
def verify_action(options) #:nodoc:
if prereqs_invalid?(options)
flash.update(options[:add_flash]) if options[:add_flash]
response.headers.update(options[:add_headers]) if options[:add_headers]
response.headers.merge!(options[:add_headers]) if options[:add_headers]
apply_remaining_actions(options) unless performed?
end
end

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004-2008 David Heinemeier Hansson
# Copyright (c) 2004-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the

View file

@ -1,8 +1,8 @@
module ActionPack #:nodoc:
module VERSION #:nodoc:
MAJOR = 2
MINOR = 2
TINY = 2
MINOR = 3
TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.')
end

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004-2008 David Heinemeier Hansson
# Copyright (c) 2004-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -31,23 +31,28 @@ rescue LoadError
end
end
require 'action_view/template_handlers'
require 'action_view/renderable'
require 'action_view/renderable_partial'
module ActionView
def self.load_all!
[Base, InlineTemplate, TemplateError]
end
require 'action_view/template'
require 'action_view/inline_template'
require 'action_view/paths'
autoload :Base, 'action_view/base'
autoload :Helpers, 'action_view/helpers'
autoload :InlineTemplate, 'action_view/inline_template'
autoload :Partials, 'action_view/partials'
autoload :PathSet, 'action_view/paths'
autoload :Renderable, 'action_view/renderable'
autoload :RenderablePartial, 'action_view/renderable_partial'
autoload :Template, 'action_view/template'
autoload :ReloadableTemplate, 'action_view/reloadable_template'
autoload :TemplateError, 'action_view/template_error'
autoload :TemplateHandler, 'action_view/template_handler'
autoload :TemplateHandlers, 'action_view/template_handlers'
autoload :Helpers, 'action_view/helpers'
autoload :SafeBuffer, 'action_view/safe_buffer'
end
require 'action_view/erb/util'
require 'action_view/base'
require 'action_view/partials'
require 'action_view/template_error'
I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml"
require 'action_view/helpers'
ActionView::Base.class_eval do
include ActionView::Partials
include ActionView::Helpers
end

View file

@ -3,9 +3,12 @@ module ActionView #:nodoc:
end
class MissingTemplate < ActionViewError #:nodoc:
attr_reader :path
def initialize(paths, path, template_format = nil)
@path = path
full_template_path = path.include?('.') ? path : "#{path}.erb"
display_paths = paths.join(':')
display_paths = paths.compact.join(":")
template_type = (path =~ /layouts/i) ? 'layout' : 'template'
super("Missing #{template_type} #{full_template_path} in view path #{display_paths}")
end
@ -157,7 +160,7 @@ module ActionView #:nodoc:
#
# See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
class Base
include ERB::Util
include Helpers, Partials, ::ERB::Util
extend ActiveSupport::Memoizable
attr_accessor :base_path, :assigns, :template_extension
@ -172,29 +175,30 @@ module ActionView #:nodoc:
delegate :logger, :to => 'ActionController::Base'
end
# Templates that are exempt from layouts
@@exempt_from_layout = Set.new([/\.rjs$/])
# Don't render layouts for templates with the given extensions.
def self.exempt_from_layout(*extensions)
regexps = extensions.collect do |extension|
extension.is_a?(Regexp) ? extension : /\.#{Regexp.escape(extension.to_s)}$/
end
@@exempt_from_layout.merge(regexps)
end
@@debug_rjs = false
##
# :singleton-method:
# Specify whether RJS responses should be wrapped in a try/catch block
# that alert()s the caught exception (and then re-raises it).
@@debug_rjs = false
cattr_accessor :debug_rjs
# A warning will be displayed whenever an action results in a cache miss on your view paths.
@@warn_cache_misses = false
cattr_accessor :warn_cache_misses
# Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed.
# Automatically reloading templates are not thread safe and should only be used in development mode.
@@cache_template_loading = nil
cattr_accessor :cache_template_loading
# :nodoc:
def self.xss_safe?
false
end
def self.cache_template_loading?
ActionController::Base.allow_concurrency || (cache_template_loading.nil? ? !ActiveSupport::Dependencies.load? : cache_template_loading)
end
attr_internal :request
delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers,
delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers,
:flash, :logger, :action_name, :controller_name, :to => :controller
module CompiledTemplates #:nodoc:
@ -222,38 +226,43 @@ module ActionView #:nodoc:
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render
@assigns_added = nil
@_render_stack = []
@controller = controller
@helpers = ProxyModule.new(self)
self.view_paths = view_paths
@_first_render = nil
@_current_render = nil
end
attr_reader :view_paths
def view_paths=(paths)
@view_paths = self.class.process_view_paths(paths)
# we might be using ReloadableTemplates, so we need to let them know this a new request
@view_paths.load!
end
# Renders the template present at <tt>template_path</tt> (relative to the view_paths array).
# The hash in <tt>local_assigns</tt> is made available as local variables.
# Returns the result of a render that's dictated by the options hash. The primary options are:
#
# * <tt>:partial</tt> - See ActionView::Partials.
# * <tt>:update</tt> - Calls update_page with the block given.
# * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
# * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
# * <tt>:text</tt> - Renders the text passed in out.
#
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
# as the locals hash.
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
if options.is_a?(String)
ActiveSupport::Deprecation.warn(
"Calling render with a string will render a partial from Rails 2.3. " +
"Change this call to render(:file => '#{options}', :locals => locals_hash)."
)
render(:file => options, :locals => local_assigns)
elsif options == :update
update_page(&block)
elsif options.is_a?(Hash)
case options
when Hash
options = options.reverse_merge(:locals => {})
if options[:layout]
_render_with_layout(options, local_assigns, &block)
elsif options[:file]
_pick_template(options[:file]).render_template(self, options[:locals])
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
render_partial(options)
elsif options[:inline]
@ -261,6 +270,10 @@ module ActionView #:nodoc:
elsif options[:text]
options[:text]
end
when :update
update_page(&block)
else
render_partial(:partial => options, :locals => local_assigns)
end
end
@ -271,7 +284,7 @@ module ActionView #:nodoc:
if defined? @template_format
@template_format
elsif controller && controller.respond_to?(:request)
@template_format = controller.request.template_format
@template_format = controller.request.template_format.to_sym
else
@template_format = :html
end
@ -280,7 +293,19 @@ module ActionView #:nodoc:
# Access the current template being rendered.
# Returns a ActionView::Template object.
def template
@_render_stack.last
@_current_render
end
def template=(template) #:nodoc:
@_first_render ||= template
@_current_render = template
end
def with_template(current_template)
last_template, self.template = template, current_template
yield
ensure
self.template = last_template
end
private
@ -307,50 +332,6 @@ module ActionView #:nodoc:
end
end
def _pick_template(template_path)
return template_path if template_path.respond_to?(:render)
path = template_path.sub(/^\//, '')
if m = path.match(/(.*)\.(\w+)$/)
template_file_name, template_file_extension = m[1], m[2]
else
template_file_name = path
end
# OPTIMIZE: Checks to lookup template in view path
if template = self.view_paths["#{template_file_name}.#{template_format}"]
template
elsif template = self.view_paths[template_file_name]
template
elsif (first_render = @_render_stack.first) && first_render.respond_to?(:format_and_extension) &&
(template = self.view_paths["#{template_file_name}.#{first_render.format_and_extension}"])
template
elsif template_format == :js && template = self.view_paths["#{template_file_name}.html"]
@template_format = :html
template
else
template = Template.new(template_path, view_paths)
if self.class.warn_cache_misses && logger
logger.debug "[PERFORMANCE] Rendering a template that was " +
"not found in view path. Templates outside the view path are " +
"not cached and result in expensive disk operations. Move this " +
"file into #{view_paths.join(':')} or add the folder to your " +
"view path list"
end
template
end
end
memoize :_pick_template
def _exempt_from_layout?(template_path) #:nodoc:
template = _pick_template(template_path).to_s
@@exempt_from_layout.any? { |ext| template =~ ext }
rescue ActionView::MissingTemplate
return false
end
def _render_with_layout(options, local_assigns, &block) #:nodoc:
partial_layout = options.delete(:layout)

View file

@ -0,0 +1,44 @@
require 'erb'
class ERB
module Util
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
#
# In your ERb templates, use this method to escape any unsafe content. For example:
# <%=h @person.name %>
#
# ==== Example:
# puts html_escape("is a > 0 & a < 10?")
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
end
undef :h
alias h html_escape
module_function :html_escape
module_function :h
# A utility method for escaping HTML entities in JSON strings.
# This method is also aliased as <tt>j</tt>.
#
# In your ERb templates, use this method to escape any HTML entities:
# <%=j @person.to_json %>
#
# ==== Example:
# puts json_escape("is a > 0 & a < 10?")
# # => is a \u003E 0 \u0026 a \u003C 10?
def json_escape(s)
s.to_s.gsub(/[&"><]/) { |special| JSON_ESCAPE[special] }
end
alias j json_escape
module_function :j
module_function :json_escape
end
end

View file

@ -1,10 +1,29 @@
Dir.entries(File.expand_path("#{File.dirname(__FILE__)}/helpers")).sort.each do |file|
next unless file =~ /^([a-z][a-z_]*_helper).rb$/
require "action_view/helpers/#{$1}"
end
module ActionView #:nodoc:
module Helpers #:nodoc:
autoload :ActiveRecordHelper, 'action_view/helpers/active_record_helper'
autoload :AssetTagHelper, 'action_view/helpers/asset_tag_helper'
autoload :AtomFeedHelper, 'action_view/helpers/atom_feed_helper'
autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper'
autoload :CacheHelper, 'action_view/helpers/cache_helper'
autoload :CaptureHelper, 'action_view/helpers/capture_helper'
autoload :DateHelper, 'action_view/helpers/date_helper'
autoload :DebugHelper, 'action_view/helpers/debug_helper'
autoload :FormHelper, 'action_view/helpers/form_helper'
autoload :FormOptionsHelper, 'action_view/helpers/form_options_helper'
autoload :FormTagHelper, 'action_view/helpers/form_tag_helper'
autoload :JavaScriptHelper, 'action_view/helpers/javascript_helper'
autoload :NumberHelper, 'action_view/helpers/number_helper'
autoload :PrototypeHelper, 'action_view/helpers/prototype_helper'
autoload :RawOutputHelper, 'action_view/helpers/raw_output_helper'
autoload :RecordIdentificationHelper, 'action_view/helpers/record_identification_helper'
autoload :RecordTagHelper, 'action_view/helpers/record_tag_helper'
autoload :SanitizeHelper, 'action_view/helpers/sanitize_helper'
autoload :ScriptaculousHelper, 'action_view/helpers/scriptaculous_helper'
autoload :TagHelper, 'action_view/helpers/tag_helper'
autoload :TextHelper, 'action_view/helpers/text_helper'
autoload :TranslationHelper, 'action_view/helpers/translation_helper'
autoload :UrlHelper, 'action_view/helpers/url_helper'
def self.included(base)
base.extend(ClassMethods)
end
@ -24,8 +43,10 @@ module ActionView #:nodoc:
include FormHelper
include FormOptionsHelper
include FormTagHelper
include JavaScriptHelper
include NumberHelper
include PrototypeHelper
include RawOutputHelper
include RecordIdentificationHelper
include RecordTagHelper
include SanitizeHelper

View file

@ -3,7 +3,7 @@ require 'action_view/helpers/form_helper'
module ActionView
class Base
@@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" }
@@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>".html_safe! }
cattr_accessor :field_error_proc
end
@ -121,7 +121,7 @@ module ActionView
if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
(errors = obj.errors.on(method))
content_tag("div",
"#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}",
"#{options[:prepend_text]}#{ERB::Util.html_escape(errors.is_a?(Array) ? errors.first : errors)}#{options[:append_text]}",
:class => options[:css_class]
)
else
@ -171,7 +171,7 @@ module ActionView
options = params.extract_options!.symbolize_keys
if object = options.delete(:object)
objects = [object].flatten
objects = Array.wrap(object)
else
objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
end
@ -198,7 +198,7 @@ module ActionView
locale.t :header, :count => count, :model => object_name
end
message = options.include?(:message) ? options[:message] : locale.t(:body)
error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, ERB::Util.html_escape(msg)) } }.join
contents = ''
contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
@ -290,7 +290,7 @@ module ActionView
end
def error_wrapping(html_tag, has_error)
has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
has_error ? Base.field_error_proc.call(html_tag, self).html_safe! : html_tag
end
def error_message

View file

@ -6,54 +6,70 @@ module ActionView
module Helpers #:nodoc:
# This module provides methods for generating HTML that links views to assets such
# as images, javascripts, stylesheets, and feeds. These methods do not verify
# the assets exist before linking to them.
# the assets exist before linking to them:
#
# image_tag("rails.png")
# # => <img alt="Rails src="/images/rails.png?1230601161" />
# stylesheet_link_tag("application")
# # => <link href="/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" type="text/css" />
#
# === Using asset hosts
#
# By default, Rails links to these assets on the current host in the public
# folder, but you can direct Rails to link to assets from a dedicated assets server by
# setting ActionController::Base.asset_host in your <tt>config/environment.rb</tt>. For example,
# let's say your asset host is <tt>assets.example.com</tt>.
# folder, but you can direct Rails to link to assets from a dedicated asset
# server by setting ActionController::Base.asset_host in the application
# configuration, typically in <tt>config/environments/production.rb</tt>.
# For example, you'd define <tt>assets.example.com</tt> to be your asset
# host this way:
#
# ActionController::Base.asset_host = "assets.example.com"
# image_tag("rails.png")
# => <img src="http://assets.example.com/images/rails.png" alt="Rails" />
# stylesheet_link_tag("application")
# => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
#
# This is useful since browsers typically open at most two connections to a single host,
# which means your assets often wait in single file for their turn to load. You can
# alleviate this by using a <tt>%d</tt> wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com")
# to automatically distribute asset requests among four hosts (e.g., "assets0.example.com" through "assets3.example.com")
# so browsers will open eight connections rather than two.
# Helpers take that into account:
#
# image_tag("rails.png")
# => <img src="http://assets0.example.com/images/rails.png" alt="Rails" />
# # => <img alt="Rails" src="http://assets.example.com/images/rails.png?1230601161" />
# stylesheet_link_tag("application")
# => <link href="http://assets3.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
# # => <link href="http://assets.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" type="text/css" />
#
# To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME
# the wildcard to a single asset host. You can read more about setting up your DNS CNAME records from
# your ISP.
# Browsers typically open at most two simultaneous connections to a single
# host, which means your assets often have to wait for other assets to finish
# downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
# +asset_host+. For example, "assets%d.example.com". If that wildcard is
# present Rails distributes asset requests among the corresponding four hosts
# "assets0.example.com", ..., "assets3.example.com". With this trick browsers
# will open eight simultaneous connections rather than two.
#
# image_tag("rails.png")
# # => <img alt="Rails" src="http://assets0.example.com/images/rails.png?1230601161" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" type="text/css" />
#
# To do this, you can either setup four actual hosts, or you can use wildcard
# DNS to CNAME the wildcard to a single asset host. You can read more about
# setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
# for server load balancing. See http://www.die.net/musings/page_load_time/
# for background.
#
# Alternatively, you can exert more control over the asset host by setting <tt>asset_host</tt> to a proc
# that takes a single source argument. This is useful if you are unable to setup 4 actual hosts or have
# fewer/more than 4 hosts. The example proc below generates http://assets1.example.com and
# http://assets2.example.com randomly.
# Alternatively, you can exert more control over the asset host by setting
# +asset_host+ to a proc like this:
#
# ActionController::Base.asset_host = Proc.new { |source| "http://assets#{rand(2) + 1}.example.com" }
# ActionController::Base.asset_host = Proc.new { |source|
# "http://assets#{rand(2) + 1}.example.com"
# }
# image_tag("rails.png")
# => <img src="http://assets2.example.com/images/rails.png" alt="Rails" />
# # => <img alt="Rails" src="http://assets0.example.com/images/rails.png?1230601161" />
# stylesheet_link_tag("application")
# => <link href="http://assets1.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
# # => <link href="http://assets1.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" type="text/css" />
#
# The proc takes a <tt>source</tt> parameter (which is the path of the source asset) and an optional
# <tt>request</tt> parameter (which is an entire instance of an <tt>ActionController::AbstractRequest</tt>
# subclass). This can be used to generate a particular asset host depending on the asset path and the particular
# request.
# The example above generates "http://assets1.example.com" and
# "http://assets2.example.com" randomly. This option is useful for example if
# you need fewer/more than four hosts, custom host names, etc.
#
# As you see the proc takes a +source+ parameter. That's a string with the
# absolute path of the asset with any extensions and timestamps in place,
# for example "/images/rails.png?1230601161".
#
# ActionController::Base.asset_host = Proc.new { |source|
# if source.starts_with?('/images')
@ -63,14 +79,16 @@ module ActionView
# end
# }
# image_tag("rails.png")
# => <img src="http://images.example.com/images/rails.png" alt="Rails" />
# # => <img alt="Rails" src="http://images.example.com/images/rails.png?1230601161" />
# stylesheet_link_tag("application")
# => <link href="http://assets.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
# # => <link href="http://assets.example.com/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" type="text/css" />
#
# The optional <tt>request</tt> parameter to the proc is useful in particular for serving assets from an
# SSL-protected page. The example proc below disables asset hosting for HTTPS connections, while still sending
# assets for plain HTTP requests from asset hosts. This is useful for avoiding mixed media warnings when serving
# non-HTTP assets from HTTPS web pages when you don't have an SSL certificate for each of the asset hosts.
# Alternatively you may ask for a second parameter +request+. That one is
# particularly useful for serving assets from an SSL-protected page. The
# example proc below disables asset hosting for HTTPS connections, while
# still sending assets for plain HTTP requests from asset hosts. If you don't
# have SSL certificates for each of the asset hosts this technique allows you
# to avoid warnings in the client about mixed media.
#
# ActionController::Base.asset_host = Proc.new { |source, request|
# if request.ssl?
@ -80,26 +98,38 @@ module ActionView
# end
# }
#
# You can also implement a custom asset host object that responds to +call+
# and takes either one or two parameters just like the proc.
#
# config.action_controller.asset_host = AssetHostingWithMinimumSsl.new(
# "http://asset%d.example.com", "https://asset1.example.com"
# )
#
# === Using asset timestamps
#
# By default, Rails will append all asset paths with that asset's timestamp. This allows you to set a cache-expiration date for the
# asset far into the future, but still be able to instantly invalidate it by simply updating the file (and hence updating the timestamp,
# which then updates the URL as the timestamp is part of that, which in turn busts the cache).
# By default, Rails appends asset's timestamps to all asset paths. This allows
# you to set a cache-expiration date for the asset far into the future, but
# still be able to instantly invalidate it by simply updating the file (and
# hence updating the timestamp, which then updates the URL as the timestamp
# is part of that, which in turn busts the cache).
#
# It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take
# advantage of this feature. Here's an example for Apache:
# It's the responsibility of the web server you use to set the far-future
# expiration date on cache assets that you need to take advantage of this
# feature. Here's an example for Apache:
#
# # Asset Expiration
# ExpiresActive On
# <FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
# ExpiresDefault "access plus 1 year"
# </FilesMatch>
# # Asset Expiration
# ExpiresActive On
# <FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
# ExpiresDefault "access plus 1 year"
# </FilesMatch>
#
# Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must
# have their clocks synchronized. If one of them drift out of sync, you'll see different timestamps at random and the cache won't
# work. Which means that the browser will request the same assets over and over again even thought they didn't change. You can use
# something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being
# requested over and over).
# Also note that in order for this to work, all your application servers must
# return the same timestamps. This means that they must have their clocks
# synchronized. If one of them drifts out of sync, you'll see different
# timestamps at random and the cache won't work. In that case the browser
# will request the same assets over and over again even thought they didn't
# change. You can use something like Live HTTP Headers for Firefox to verify
# that the cache is indeed working.
module AssetTagHelper
ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public"
JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts"
@ -111,7 +141,7 @@ module ActionView
# <tt>:atom</tt>. Control the link options in url_for format using the
# +url_options+. You can modify the LINK tag itself in +tag_options+.
#
# ==== Options:
# ==== Options
# * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
# * <tt>:type</tt> - Override the auto-generated mime type
# * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
@ -151,7 +181,7 @@ module ActionView
# javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js
# javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
def javascript_path(source)
JavaScriptTag.new(self, @controller, source).public_path
compute_public_path(source, 'javascripts', 'js')
end
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
@ -242,24 +272,25 @@ module ActionView
# javascript_include_tag :all, :cache => true, :recursive => true
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
cache = options.delete("cache")
concat = options.delete("concat")
cache = concat || options.delete("cache")
recursive = options.delete("recursive")
if ActionController::Base.perform_caching && cache
if concat || (ActionController::Base.perform_caching && cache)
joined_javascript_name = (cache == true ? "all" : cache) + ".js"
joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name)
joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : JAVASCRIPTS_DIR, joined_javascript_name)
unless File.exists?(joined_javascript_path)
JavaScriptSources.create(self, @controller, sources, recursive).write_asset_file_contents(joined_javascript_path)
unless ActionController::Base.perform_caching && File.exists?(joined_javascript_path)
write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive))
end
javascript_src_tag(joined_javascript_name, options)
else
JavaScriptSources.create(self, @controller, sources, recursive).expand_sources.collect { |source|
javascript_src_tag(source, options)
}.join("\n")
expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe!
end
end
@@javascript_expansions = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
# Register one or more javascript files to be included when <tt>symbol</tt>
# is passed to <tt>javascript_include_tag</tt>. This method is typically intended
# to be called from plugin initialization to register javascript files
@ -272,9 +303,11 @@ module ActionView
# <script type="text/javascript" src="/javascripts/body.js"></script>
# <script type="text/javascript" src="/javascripts/tail.js"></script>
def self.register_javascript_expansion(expansions)
JavaScriptSources.expansions.merge!(expansions)
@@javascript_expansions.merge!(expansions)
end
@@stylesheet_expansions = {}
# Register one or more stylesheet files to be included when <tt>symbol</tt>
# is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended
# to be called from plugin initialization to register stylesheet files
@ -287,7 +320,7 @@ module ActionView
# <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" />
# <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" />
def self.register_stylesheet_expansion(expansions)
StylesheetSources.expansions.merge!(expansions)
@@stylesheet_expansions.merge!(expansions)
end
# Register one or more additional JavaScript files to be included when
@ -295,11 +328,11 @@ module ActionView
# typically intended to be called from plugin initialization to register additional
# .js files that the plugin installed in <tt>public/javascripts</tt>.
def self.register_javascript_include_default(*sources)
JavaScriptSources.expansions[:defaults].concat(sources)
@@javascript_expansions[:defaults].concat(sources)
end
def self.reset_javascript_include_default #:nodoc:
JavaScriptSources.expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup
@@javascript_expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup
end
# Computes the path to a stylesheet asset in the public stylesheets directory.
@ -314,7 +347,7 @@ module ActionView
# stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css
# stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css
def stylesheet_path(source)
StylesheetTag.new(self, @controller, source).public_path
compute_public_path(source, 'stylesheets', 'css')
end
alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
@ -380,23 +413,28 @@ module ActionView
# The <tt>:recursive</tt> option is also available for caching:
#
# stylesheet_link_tag :all, :cache => true, :recursive => true
#
# To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if
# you have too many stylesheets for IE to load.
#
# stylesheet_link_tag :all, :concat => true
#
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
cache = options.delete("cache")
concat = options.delete("concat")
cache = concat || options.delete("cache")
recursive = options.delete("recursive")
if ActionController::Base.perform_caching && cache
if concat || (ActionController::Base.perform_caching && cache)
joined_stylesheet_name = (cache == true ? "all" : cache) + ".css"
joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name)
joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : STYLESHEETS_DIR, joined_stylesheet_name)
unless File.exists?(joined_stylesheet_path)
StylesheetSources.create(self, @controller, sources, recursive).write_asset_file_contents(joined_stylesheet_path)
unless ActionController::Base.perform_caching && File.exists?(joined_stylesheet_path)
write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive))
end
stylesheet_tag(joined_stylesheet_name, options)
else
StylesheetSources.create(self, @controller, sources, recursive).expand_sources.collect { |source|
stylesheet_tag(source, options)
}.join("\n")
expand_stylesheet_sources(sources, recursive).collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe!
end
end
@ -411,7 +449,7 @@ module ActionView
# image_path("/icons/edit.png") # => /icons/edit.png
# image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png
def image_path(source)
ImageTag.new(self, @controller, source).public_path
compute_public_path(source, 'images')
end
alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
@ -466,7 +504,118 @@ module ActionView
tag("img", options)
end
def self.cache_asset_timestamps
@@cache_asset_timestamps
end
# You can enable or disable the asset tag timestamps cache.
# With the cache enabled, the asset tag helper methods will make fewer
# expense file system calls. However this prevents you from modifying
# any asset files while the server is running.
#
# ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
def self.cache_asset_timestamps=(value)
@@cache_asset_timestamps = value
end
@@cache_asset_timestamps = true
private
# Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
# Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
# roots. Rewrite the asset path for cache-busting asset ids. Include
# asset host, if configured, with the correct request protocol.
def compute_public_path(source, dir, ext = nil, include_host = true)
has_request = @controller.respond_to?(:request)
source_ext = File.extname(source)[1..-1]
if ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))))
source += ".#{ext}"
end
unless source =~ %r{^[-a-z]+://}
source = "/#{dir}/#{source}" unless source[0] == ?/
source = rewrite_asset_path(source)
if has_request && include_host
unless source =~ %r{^#{ActionController::Base.relative_url_root}/}
source = "#{ActionController::Base.relative_url_root}#{source}"
end
end
end
if include_host && source !~ %r{^[-a-z]+://}
host = compute_asset_host(source)
if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
host = "#{@controller.request.protocol}#{host}"
end
"#{host}#{source}"
else
source
end
end
# Pick an asset host for this source. Returns +nil+ if no host is set,
# the host if no wildcard is set, the host interpolated with the
# numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
# or the value returned from invoking the proc if it's a proc or the value from
# invoking call if it's an object responding to call.
def compute_asset_host(source)
if host = ActionController::Base.asset_host
if host.is_a?(Proc) || host.respond_to?(:call)
case host.is_a?(Proc) ? host.arity : host.method(:call).arity
when 2
request = @controller.respond_to?(:request) && @controller.request
host.call(source, request)
else
host.call(source)
end
else
(host =~ /%d/) ? host % (source.hash % 4) : host
end
end
end
@@asset_timestamps_cache = {}
@@asset_timestamps_cache_guard = Mutex.new
# Use the RAILS_ASSET_ID environment variable or the source's
# modification time as its cache-busting asset id.
def rails_asset_id(source)
if asset_id = ENV["RAILS_ASSET_ID"]
asset_id
else
if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source])
asset_id
else
path = File.join(ASSETS_DIR, source)
asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : ''
if @@cache_asset_timestamps
@@asset_timestamps_cache_guard.synchronize do
@@asset_timestamps_cache[source] = asset_id
end
end
asset_id
end
end
end
# Break out the asset path rewrite in case plugins wish to put the asset id
# someplace other than the query string.
def rewrite_asset_path(source)
asset_id = rails_asset_id(source)
if asset_id.blank?
source
else
source + "?#{asset_id}"
end
end
def javascript_src_tag(source, options)
content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options))
end
@ -475,342 +624,70 @@ module ActionView
tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false)
end
module ImageAsset
DIRECTORY = 'images'.freeze
def compute_javascript_paths(*args)
expand_javascript_sources(*args).collect { |source| compute_public_path(source, 'javascripts', 'js', false) }
end
def directory
DIRECTORY
end
def compute_stylesheet_paths(*args)
expand_stylesheet_sources(*args).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) }
end
def extension
nil
def expand_javascript_sources(sources, recursive = false)
if sources.include?(:all)
all_javascript_files = collect_asset_files(JAVASCRIPTS_DIR, ('**' if recursive), '*.js')
((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq
else
expanded_sources = sources.collect do |source|
determine_source(source, @@javascript_expansions)
end.flatten
expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(JAVASCRIPTS_DIR, "application.js"))
expanded_sources
end
end
module JavaScriptAsset
DIRECTORY = 'javascripts'.freeze
EXTENSION = 'js'.freeze
def public_directory
JAVASCRIPTS_DIR
end
def directory
DIRECTORY
end
def extension
EXTENSION
def expand_stylesheet_sources(sources, recursive)
if sources.first == :all
collect_asset_files(STYLESHEETS_DIR, ('**' if recursive), '*.css')
else
sources.collect do |source|
determine_source(source, @@stylesheet_expansions)
end.flatten
end
end
module StylesheetAsset
DIRECTORY = 'stylesheets'.freeze
EXTENSION = 'css'.freeze
def public_directory
STYLESHEETS_DIR
end
def directory
DIRECTORY
end
def extension
EXTENSION
def determine_source(source, collection)
case source
when Symbol
collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}")
else
source
end
end
class AssetTag
extend ActiveSupport::Memoizable
Cache = {}
CacheGuard = Mutex.new
ProtocolRegexp = %r{^[-a-z]+://}.freeze
def initialize(template, controller, source, include_host = true)
# NOTE: The template arg is temporarily needed for a legacy plugin
# hook that is expected to call rewrite_asset_path on the
# template. This should eventually be removed.
@template = template
@controller = controller
@source = source
@include_host = include_host
@cache_key = if controller.respond_to?(:request)
[self.class.name,controller.request.protocol,
ActionController::Base.asset_host,
ActionController::Base.relative_url_root,
source, include_host]
else
[self.class.name,ActionController::Base.asset_host, source, include_host]
end
end
def public_path
compute_public_path(@source)
end
memoize :public_path
def asset_file_path
File.join(ASSETS_DIR, public_path.split('?').first)
end
memoize :asset_file_path
def contents
File.read(asset_file_path)
end
def mtime
File.mtime(asset_file_path)
end
private
def request
@controller.request
end
def request?
@controller.respond_to?(:request)
end
# Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
# Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
# roots. Rewrite the asset path for cache-busting asset ids. Include
# asset host, if configured, with the correct request protocol.
def compute_public_path(source)
if source =~ ProtocolRegexp
source += ".#{extension}" if missing_extension?(source)
source = prepend_asset_host(source)
source
else
CacheGuard.synchronize do
Cache[@cache_key] ||= begin
source += ".#{extension}" if missing_extension?(source) || file_exists_with_extension?(source)
source = "/#{directory}/#{source}" unless source[0] == ?/
source = rewrite_asset_path(source)
source = prepend_relative_url_root(source)
source = prepend_asset_host(source)
source
end
end
end
end
def missing_extension?(source)
extension && File.extname(source).blank?
end
def file_exists_with_extension?(source)
extension && File.exist?(File.join(ASSETS_DIR, directory, "#{source}.#{extension}"))
end
def prepend_relative_url_root(source)
relative_url_root = ActionController::Base.relative_url_root
if request? && @include_host && source !~ %r{^#{relative_url_root}/}
"#{relative_url_root}#{source}"
else
source
end
end
def prepend_asset_host(source)
if @include_host && source !~ ProtocolRegexp
host = compute_asset_host(source)
if request? && !host.blank? && host !~ ProtocolRegexp
host = "#{request.protocol}#{host}"
end
"#{host}#{source}"
else
source
end
end
# Pick an asset host for this source. Returns +nil+ if no host is set,
# the host if no wildcard is set, the host interpolated with the
# numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
# or the value returned from invoking the proc if it's a proc.
def compute_asset_host(source)
if host = ActionController::Base.asset_host
if host.is_a?(Proc)
case host.arity
when 2
host.call(source, request)
else
host.call(source)
end
else
(host =~ /%d/) ? host % (source.hash % 4) : host
end
end
end
# Use the RAILS_ASSET_ID environment variable or the source's
# modification time as its cache-busting asset id.
def rails_asset_id(source)
if asset_id = ENV["RAILS_ASSET_ID"]
asset_id
else
path = File.join(ASSETS_DIR, source)
if File.exist?(path)
File.mtime(path).to_i.to_s
else
''
end
end
end
# Break out the asset path rewrite in case plugins wish to put the asset id
# someplace other than the query string.
def rewrite_asset_path(source)
if @template.respond_to?(:rewrite_asset_path)
# DEPRECATE: This way to override rewrite_asset_path
@template.send(:rewrite_asset_path, source)
else
asset_id = rails_asset_id(source)
if asset_id.blank?
source
else
"#{source}?#{asset_id}"
end
end
end
def join_asset_file_contents(paths)
paths.collect { |path| File.read(asset_file_path(path)) }.join("\n\n")
end
class ImageTag < AssetTag
include ImageAsset
def write_asset_file_contents(joined_asset_path, asset_paths)
FileUtils.mkdir_p(File.dirname(joined_asset_path))
File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) }
# Set mtime to the latest of the combined files to allow for
# consistent ETag without a shared filesystem.
mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max
File.utime(mt, mt, joined_asset_path)
end
class JavaScriptTag < AssetTag
include JavaScriptAsset
def asset_file_path(path)
File.join(ASSETS_DIR, path.split('?').first)
end
class StylesheetTag < AssetTag
include StylesheetAsset
end
def collect_asset_files(*path)
dir = path.first
class AssetCollection
extend ActiveSupport::Memoizable
Cache = {}
CacheGuard = Mutex.new
def self.create(template, controller, sources, recursive)
CacheGuard.synchronize do
key = [self, sources, recursive]
Cache[key] ||= new(template, controller, sources, recursive).freeze
end
end
def initialize(template, controller, sources, recursive)
# NOTE: The template arg is temporarily needed for a legacy plugin
# hook. See NOTE under AssetTag#initialize for more details
@template = template
@controller = controller
@sources = sources
@recursive = recursive
end
def write_asset_file_contents(joined_asset_path)
FileUtils.mkdir_p(File.dirname(joined_asset_path))
File.open(joined_asset_path, "w+") { |cache| cache.write(joined_contents) }
mt = latest_mtime
File.utime(mt, mt, joined_asset_path)
end
private
def determine_source(source, collection)
case source
when Symbol
collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}")
else
source
end
end
def validate_sources!
@sources.collect { |source| determine_source(source, self.class.expansions) }.flatten
end
def all_asset_files
path = [public_directory, ('**' if @recursive), "*.#{extension}"].compact
Dir[File.join(*path)].collect { |file|
file[-(file.size - public_directory.size - 1)..-1].sub(/\.\w+$/, '')
}.sort
end
def tag_sources
expand_sources.collect { |source| tag_class.new(@template, @controller, source, false) }
end
def joined_contents
tag_sources.collect { |source| source.contents }.join("\n\n")
end
# Set mtime to the latest of the combined files to allow for
# consistent ETag without a shared filesystem.
def latest_mtime
tag_sources.map { |source| source.mtime }.max
end
end
class JavaScriptSources < AssetCollection
include JavaScriptAsset
EXPANSIONS = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
def self.expansions
EXPANSIONS
end
APPLICATION_JS = "application".freeze
APPLICATION_FILE = "application.js".freeze
def expand_sources
if @sources.include?(:all)
assets = all_asset_files
((defaults.dup & assets) + assets).uniq!
else
expanded_sources = validate_sources!
expanded_sources << APPLICATION_JS if include_application?
expanded_sources
end
end
memoize :expand_sources
private
def tag_class
JavaScriptTag
end
def defaults
determine_source(:defaults, self.class.expansions)
end
def include_application?
@sources.include?(:defaults) && File.exist?(File.join(JAVASCRIPTS_DIR, APPLICATION_FILE))
end
end
class StylesheetSources < AssetCollection
include StylesheetAsset
EXPANSIONS = {}
def self.expansions
EXPANSIONS
end
def expand_sources
@sources.first == :all ? all_asset_files : validate_sources!
end
memoize :expand_sources
private
def tag_class
StylesheetTag
end
Dir[File.join(*path.compact)].collect do |file|
file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '')
end.sort
end
end
end

View file

@ -30,7 +30,7 @@ module ActionView
# app/views/posts/index.atom.builder:
# atom_feed do |feed|
# feed.title("My great blog!")
# feed.updated((@posts.first.created_at))
# feed.updated(@posts.first.created_at)
#
# for post in @posts
# feed.entry(post) do |entry|
@ -98,7 +98,7 @@ module ActionView
options[:schema_date] = "2005" # The Atom spec copyright date
end
xml = options[:xml] || eval("xml", block.binding)
xml = options.delete(:xml) || eval("xml", block.binding)
xml.instruct!
if options[:instruct]
options[:instruct].each do |target,attrs|

View file

@ -18,16 +18,37 @@ module ActionView
# That would add something like "Process data files (345.2ms)" to the log,
# which you can then use to compare timings when optimizing your code.
#
# You may give an optional logger level as the second argument
# You may give an optional logger level as the :level option.
# (:debug, :info, :warn, :error); the default value is :info.
def benchmark(message = "Benchmarking", level = :info)
#
# <% benchmark "Low-level files", :level => :debug do %>
# <%= lowlevel_files_operation %>
# <% end %>
#
# Finally, you can pass true as the third argument to silence all log activity
# inside the block. This is great for boiling down a noisy block to just a single statement:
#
# <% benchmark "Process data files", :level => :info, :silence => true do %>
# <%= expensive_and_chatty_files_operation %>
# <% end %>
def benchmark(message = "Benchmarking", options = {})
if controller.logger
seconds = Benchmark.realtime { yield }
controller.logger.send(level, "#{message} (#{'%.1f' % (seconds * 1000)}ms)")
if options.is_a?(Symbol)
ActiveSupport::Deprecation.warn("use benchmark('#{message}', :level => :#{options}) instead", caller)
options = { :level => options, :silence => false }
else
options.assert_valid_keys(:level, :silence)
options[:level] ||= :info
end
result = nil
ms = Benchmark.ms { result = options[:silence] ? controller.logger.silence { yield } : yield }
controller.logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])
result
else
yield
end
end
end
end
end
end

View file

@ -118,13 +118,13 @@ module ActionView
def content_for(name, content = nil, &block)
ivar = "@content_for_#{name}"
content = capture(&block) if block_given?
instance_variable_set(ivar, "#{instance_variable_get(ivar)}#{content}")
instance_variable_set(ivar, "#{instance_variable_get(ivar)}#{content}".html_safe!)
nil
end
# Use an alternate output buffer for the duration of the block.
# Defaults to a new empty string.
def with_output_buffer(buf = '') #:nodoc:
def with_output_buffer(buf = "") #:nodoc:
self.output_buffer, old_buffer = buf, output_buffer
yield
output_buffer

View file

@ -26,8 +26,10 @@ module ActionView
# 47 hrs, 59 mins, 29 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
# 29 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 1 month
# 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
# 1 yr <-> 2 yrs minus 1 secs # => about 1 year
# 2 yrs <-> max time or date # => over [2..X] years
# 1 yr <-> 1 yr, 3 months # => about 1 year
# 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year
# 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years
# 2 yrs <-> max time or date # => (same rules as 1 yr)
#
# With <tt>include_seconds</tt> = true and the difference < 1 minute 29 seconds:
# 0-4 secs # => less than 5 seconds
@ -43,17 +45,18 @@ module ActionView
# distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
# distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
# distance_of_time_in_words(from_time, from_time + 15.seconds, true) # => less than 20 seconds
# distance_of_time_in_words(from_time, 3.years.from_now) # => over 3 years
# distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
# distance_of_time_in_words(from_time, from_time + 60.hours) # => about 3 days
# distance_of_time_in_words(from_time, from_time + 45.seconds, true) # => less than a minute
# distance_of_time_in_words(from_time, from_time - 45.seconds, true) # => less than a minute
# distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
# distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
# distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => over 4 years
# distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
# distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
#
# to_time = Time.now + 6.years + 19.days
# distance_of_time_in_words(from_time, to_time, true) # => over 6 years
# distance_of_time_in_words(to_time, from_time, true) # => over 6 years
# distance_of_time_in_words(from_time, to_time, true) # => about 6 years
# distance_of_time_in_words(to_time, from_time, true) # => about 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
#
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
@ -81,12 +84,21 @@ module ActionView
when 2..44 then locale.t :x_minutes, :count => distance_in_minutes
when 45..89 then locale.t :about_x_hours, :count => 1
when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
when 1440..2879 then locale.t :x_days, :count => 1
when 2880..43199 then locale.t :x_days, :count => (distance_in_minutes / 1440).round
when 1440..2529 then locale.t :x_days, :count => 1
when 2530..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round
when 43200..86399 then locale.t :about_x_months, :count => 1
when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes / 43200).round
when 525600..1051199 then locale.t :about_x_years, :count => 1
else locale.t :over_x_years, :count => (distance_in_minutes / 525600).round
when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round
else
distance_in_years = distance_in_minutes / 525600
minute_offset_for_leap_year = (distance_in_years / 4) * 1440
remainder = ((distance_in_minutes - minute_offset_for_leap_year) % 525600)
if remainder < 131400
locale.t(:about_x_years, :count => distance_in_years)
elsif remainder < 394200
locale.t(:over_x_years, :count => distance_in_years)
else
locale.t(:almost_x_years, :count => distance_in_years + 1)
end
end
end
end
@ -111,7 +123,7 @@ module ActionView
#
# ==== Options
# * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
# "2" instead of "February").
# "2" instead of "February").
# * <tt>:use_short_month</tt> - Set to true if you want to use the abbreviated month name instead of the full
# name (e.g. "Feb" instead of "February").
# * <tt>:add_month_number</tt> - Set to true if you want to show both, the month's number and name (e.g.
@ -136,6 +148,10 @@ module ActionView
# dates.
# * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil.
# * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
# * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
# for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
# Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds)
# or the given prompt string.
#
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
#
@ -171,6 +187,9 @@ module ActionView
# # that will have a default day of 20.
# date_select("credit_card", "bill_due", :default => { :day => 20 })
#
# # Generates a date select with custom prompts
# date_select("post", "written_on", :prompt => { :day => 'Select day', :month => 'Select month', :year => 'Select year' })
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
@ -210,6 +229,11 @@ module ActionView
# # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45.
# time_select 'game', 'game_time', {:minute_step => 15}
#
# # Creates a time select tag with a custom prompt. Use :prompt => true for generic prompts.
# time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
# time_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours
# time_select("post", "written_on", :prompt => true) # generic prompts for all
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
@ -241,6 +265,11 @@ module ActionView
# # as the written_on attribute.
# datetime_select("post", "written_on", :discard_type => true)
#
# # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts.
# datetime_select("post", "written_on", :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
# datetime_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours
# datetime_select("post", "written_on", :prompt => true) # generic prompts for all
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def datetime_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options)
@ -285,6 +314,11 @@ module ActionView
# # prefixed with 'payday' rather than 'date'
# select_datetime(my_date_time, :prefix => 'payday')
#
# # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts.
# select_datetime(my_date_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
# select_datetime(my_date_time, :prompt => {:hour => true}) # generic prompt for hours
# select_datetime(my_date_time, :prompt => true) # generic prompts for all
#
def select_datetime(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_datetime
end
@ -321,6 +355,11 @@ module ActionView
# # prefixed with 'payday' rather than 'date'
# select_date(my_date, :prefix => 'payday')
#
# # Generates a date select with a custom prompt. Use :prompt=>true for generic prompts.
# select_date(my_date, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
# select_date(my_date, :prompt => {:hour => true}) # generic prompt for hours
# select_date(my_date, :prompt => true) # generic prompts for all
#
def select_date(date = Date.current, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_date
end
@ -352,6 +391,11 @@ module ActionView
# # separated by ':' and includes an input for seconds
# select_time(my_time, :time_separator => ':', :include_seconds => true)
#
# # Generates a time select with a custom prompt. Use :prompt=>true for generic prompts.
# select_time(my_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
# select_time(my_time, :prompt => {:hour => true}) # generic prompt for hours
# select_time(my_time, :prompt => true) # generic prompts for all
#
def select_time(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_time
end
@ -373,6 +417,10 @@ module ActionView
# # that is named 'interval' rather than 'second'
# select_second(my_time, :field_name => 'interval')
#
# # Generates a select field for seconds with a custom prompt. Use :prompt=>true for a
# # generic prompt.
# select_minute(14, :prompt => 'Choose seconds')
#
def select_second(datetime, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_second
end
@ -395,6 +443,10 @@ module ActionView
# # that is named 'stride' rather than 'second'
# select_minute(my_time, :field_name => 'stride')
#
# # Generates a select field for minutes with a custom prompt. Use :prompt=>true for a
# # generic prompt.
# select_minute(14, :prompt => 'Choose minutes')
#
def select_minute(datetime, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_minute
end
@ -416,6 +468,10 @@ module ActionView
# # that is named 'stride' rather than 'second'
# select_hour(my_time, :field_name => 'stride')
#
# # Generates a select field for hours with a custom prompt. Use :prompt => true for a
# # generic prompt.
# select_hour(13, :prompt =>'Choose hour')
#
def select_hour(datetime, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_hour
end
@ -437,6 +493,10 @@ module ActionView
# # that is named 'due' rather than 'day'
# select_day(my_time, :field_name => 'due')
#
# # Generates a select field for days with a custom prompt. Use :prompt => true for a
# # generic prompt.
# select_day(5, :prompt => 'Choose day')
#
def select_day(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_day
end
@ -475,6 +535,10 @@ module ActionView
# # will use keys like "Januar", "Marts."
# select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
#
# # Generates a select field for months with a custom prompt. Use :prompt => true for a
# # generic prompt.
# select_month(14, :prompt => 'Choose month')
#
def select_month(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_month
end
@ -502,6 +566,10 @@ module ActionView
# # has ascending year values
# select_year(2006, :start_year => 2000, :end_year => 2010)
#
# # Generates a select field for years with a custom prompt. Use :prompt => true for a
# # generic prompt.
# select_year(14, :prompt => 'Choose year')
#
def select_year(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_year
end
@ -764,11 +832,30 @@ module ActionView
select_html = "\n"
select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
select_html << select_options_as_html.to_s
content_tag(:select, select_html, select_options) + "\n"
end
# Builds a prompt option tag with supplied options or from default options
# prompt_option_tag(:month, :prompt => 'Select month')
# => "<option value="">Select month</option>"
def prompt_option_tag(type, options)
default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
case options
when Hash
prompt = default_options.merge(options)[type.to_sym]
when String
prompt = options
else
prompt = I18n.translate(('datetime.prompts.' + type.to_s).to_sym, :locale => @options[:locale])
end
prompt ? content_tag(:option, prompt, :value => '') : ''
end
# Builds hidden input tag for date part and value
# build_hidden(:year, 2008)
# => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
@ -785,7 +872,7 @@ module ActionView
# => post[written_on(1i)]
def input_name_from_type(type)
prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
prefix += "[#{@options[:index]}]" if @options[:index]
prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
field_name = @options[:field_name] || type
if @options[:include_position]
@ -801,8 +888,8 @@ module ActionView
input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
end
# Given an ordering of datetime components, create the selection html
# and join them with their appropriate seperators
# Given an ordering of datetime components, create the selection HTML
# and join them with their appropriate separators.
def build_selects_from_types(order)
select = ''
order.reverse.each do |type|
@ -829,15 +916,15 @@ module ActionView
class InstanceTag #:nodoc:
def to_date_select_tag(options = {}, html_options = {})
datetime_selector(options, html_options).select_date
datetime_selector(options, html_options).select_date.html_safe!
end
def to_time_select_tag(options = {}, html_options = {})
datetime_selector(options, html_options).select_time
datetime_selector(options, html_options).select_time.html_safe!
end
def to_datetime_select_tag(options = {}, html_options = {})
datetime_selector(options, html_options).select_datetime
datetime_selector(options, html_options).select_datetime.html_safe!
end
private
@ -848,7 +935,7 @@ module ActionView
options[:field_name] = @method_name
options[:include_position] = true
options[:prefix] ||= @object_name
options[:index] ||= @auto_index
options[:index] = @auto_index if defined?(@auto_index) && @auto_index && !options.has_key?(:index)
options[:datetime_separator] ||= ' &mdash; '
options[:time_separator] ||= ' : '
@ -856,7 +943,7 @@ module ActionView
end
def default_datetime(options)
return if options[:include_blank]
return if options[:include_blank] || options[:prompt]
case options[:default]
when nil
@ -886,15 +973,15 @@ module ActionView
class FormBuilder
def date_select(method, options = {}, html_options = {})
@template.date_select(@object_name, method, options.merge(:object => @object), html_options)
@template.date_select(@object_name, method, objectify_options(options), html_options)
end
def time_select(method, options = {}, html_options = {})
@template.time_select(@object_name, method, options.merge(:object => @object), html_options)
@template.time_select(@object_name, method, objectify_options(options), html_options)
end
def datetime_select(method, options = {}, html_options = {})
@template.datetime_select(@object_name, method, options.merge(:object => @object), html_options)
@template.datetime_select(@object_name, method, objectify_options(options), html_options)
end
end
end

View file

@ -5,17 +5,24 @@ require 'action_view/helpers/form_tag_helper'
module ActionView
module Helpers
# Form helpers are designed to make working with models much easier compared to using just standard HTML
# elements by providing a set of methods for creating forms based on your models. This helper generates the HTML
# for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form
# is submitted (i.e., when the user hits the submit button or <tt>form.submit</tt> is called via JavaScript), the form inputs will be bundled into the <tt>params</tt> object and passed back to the controller.
# Form helpers are designed to make working with models much easier
# compared to using just standard HTML elements by providing a set of
# methods for creating forms based on your models. This helper generates
# the HTML for forms, providing a method for each sort of input
# (e.g., text, password, select, and so on). When the form is submitted
# (i.e., when the user hits the submit button or <tt>form.submit</tt> is
# called via JavaScript), the form inputs will be bundled into the
# <tt>params</tt> object and passed back to the controller.
#
# There are two types of form helpers: those that specifically work with model attributes and those that don't.
# This helper deals with those that work with model attributes; to see an example of form helpers that don't work
# with model attributes, check the ActionView::Helpers::FormTagHelper documentation.
# There are two types of form helpers: those that specifically work with
# model attributes and those that don't. This helper deals with those that
# work with model attributes; to see an example of form helpers that don't
# work with model attributes, check the ActionView::Helpers::FormTagHelper
# documentation.
#
# The core method of this helper, form_for, gives you the ability to create a form for a model instance;
# for example, let's say that you have a model <tt>Person</tt> and want to create a new instance of it:
# The core method of this helper, form_for, gives you the ability to create
# a form for a model instance; for example, let's say that you have a model
# <tt>Person</tt> and want to create a new instance of it:
#
# # Note: a @person variable will have been created in the controller.
# # For example: @person = Person.new
@ -40,17 +47,22 @@ module ActionView
# <%= submit_tag 'Create' %>
# <% end %>
#
# This example will render the <tt>people/_form</tt> partial, setting a local variable called <tt>form</tt> which references the yielded FormBuilder.
#
# The <tt>params</tt> object created when this form is submitted would look like:
# This example will render the <tt>people/_form</tt> partial, setting a
# local variable called <tt>form</tt> which references the yielded
# FormBuilder. The <tt>params</tt> object created when this form is
# submitted would look like:
#
# {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}}
#
# The params hash has a nested <tt>person</tt> value, which can therefore be accessed with <tt>params[:person]</tt> in the controller.
# If were editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than <tt>Person.new</tt> in the controller), the objects
# attribute values are filled into the form (e.g., the <tt>person_first_name</tt> field would have that person's first name in it).
# The params hash has a nested <tt>person</tt> value, which can therefore
# be accessed with <tt>params[:person]</tt> in the controller. If were
# editing/updating an instance (e.g., <tt>Person.find(1)</tt> rather than
# <tt>Person.new</tt> in the controller), the objects attribute values are
# filled into the form (e.g., the <tt>person_first_name</tt> field would
# have that person's first name in it).
#
# If the object name contains square brackets the id for the object will be inserted. For example:
# If the object name contains square brackets the id for the object will be
# inserted. For example:
#
# <%= text_field "person[]", "name" %>
#
@ -58,8 +70,10 @@ module ActionView
#
# <input type="text" id="person_<%= @person.id %>_name" name="person[<%= @person.id %>][name]" value="<%= @person.name %>" />
#
# If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial
# used by <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may come in handy. Example:
# If the helper is being used to generate a repetitive sequence of similar
# form elements, for example in a partial used by
# <tt>render_collection_of_partials</tt>, the <tt>index</tt> option may
# come in handy. Example:
#
# <%= text_field "person", "name", "index" => 1 %>
#
@ -67,14 +81,17 @@ module ActionView
#
# <input type="text" id="person_1_name" name="person[1][name]" value="<%= @person.name %>" />
#
# An <tt>index</tt> option may also be passed to <tt>form_for</tt> and <tt>fields_for</tt>. This automatically applies
# the <tt>index</tt> to all the nested fields.
# An <tt>index</tt> option may also be passed to <tt>form_for</tt> and
# <tt>fields_for</tt>. This automatically applies the <tt>index</tt> to
# all the nested fields.
#
# There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
# link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
# There are also methods for helping to build form tags in
# link:classes/ActionView/Helpers/FormOptionsHelper.html,
# link:classes/ActionView/Helpers/DateHelper.html, and
# link:classes/ActionView/Helpers/ActiveRecordHelper.html
module FormHelper
# Creates a form and a scope around a specific model object that is used as
# a base for questioning about values for the fields.
# Creates a form and a scope around a specific model object that is used
# as a base for questioning about values for the fields.
#
# Rails provides succinct resource-oriented form generation with +form_for+
# like this:
@ -86,13 +103,15 @@ module ActionView
# <%= f.text_field :author %><br />
# <% end %>
#
# There, +form_for+ is able to generate the rest of RESTful form parameters
# based on introspection on the record, but to understand what it does we
# need to dig first into the alternative generic usage it is based upon.
# There, +form_for+ is able to generate the rest of RESTful form
# parameters based on introspection on the record, but to understand what
# it does we need to dig first into the alternative generic usage it is
# based upon.
#
# === Generic form_for
#
# The generic way to call +form_for+ yields a form builder around a model:
# The generic way to call +form_for+ yields a form builder around a
# model:
#
# <% form_for :person, :url => { :action => "update" } do |f| %>
# <%= f.error_messages %>
@ -103,8 +122,8 @@ module ActionView
# <% end %>
#
# There, the first argument is a symbol or string with the name of the
# object the form is about, and also the name of the instance variable the
# object is stored in.
# object the form is about, and also the name of the instance variable
# the object is stored in.
#
# The form builder acts as a regular form helper that somehow carries the
# model. Thus, the idea is that
@ -137,17 +156,18 @@ module ActionView
# In any of its variants, the rightmost argument to +form_for+ is an
# optional hash of options:
#
# * <tt>:url</tt> - The URL the form is submitted to. It takes the same fields
# you pass to +url_for+ or +link_to+. In particular you may pass here a
# named route directly as well. Defaults to the current action.
# * <tt>:url</tt> - The URL the form is submitted to. It takes the same
# fields you pass to +url_for+ or +link_to+. In particular you may pass
# here a named route directly as well. Defaults to the current action.
# * <tt>:html</tt> - Optional HTML attributes for the form tag.
#
# Worth noting is that the +form_for+ tag is called in a ERb evaluation block,
# not an ERb output block. So that's <tt><% %></tt>, not <tt><%= %></tt>.
# Worth noting is that the +form_for+ tag is called in a ERb evaluation
# block, not an ERb output block. So that's <tt><% %></tt>, not
# <tt><%= %></tt>.
#
# Also note that +form_for+ doesn't create an exclusive scope. It's still
# possible to use both the stand-alone FormHelper methods and methods from
# FormTagHelper. For example:
# possible to use both the stand-alone FormHelper methods and methods
# from FormTagHelper. For example:
#
# <% form_for :person, @person, :url => { :action => "update" } do |f| %>
# First name: <%= f.text_field :first_name %>
@ -156,16 +176,16 @@ module ActionView
# Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %>
# <% end %>
#
# This also works for the methods in FormOptionHelper and DateHelper that are
# designed to work with an object as base, like FormOptionHelper#collection_select
# and DateHelper#datetime_select.
# This also works for the methods in FormOptionHelper and DateHelper that
# are designed to work with an object as base, like
# FormOptionHelper#collection_select and DateHelper#datetime_select.
#
# === Resource-oriented style
#
# As we said above, in addition to manually configuring the +form_for+ call,
# you can rely on automated resource identification, which will use the conventions
# and named routes of that approach. This is the preferred way to use +form_for+
# nowadays.
# As we said above, in addition to manually configuring the +form_for+
# call, you can rely on automated resource identification, which will use
# the conventions and named routes of that approach. This is the
# preferred way to use +form_for+ nowadays.
#
# For example, if <tt>@post</tt> is an existing record you want to edit
#
@ -205,8 +225,10 @@ module ActionView
#
# === Customized form builders
#
# You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers,
# then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs.
# You can also build forms using a customized FormBuilder class. Subclass
# FormBuilder and override or define some more helpers, then use your
# custom builder. For example, let's say you made a helper to
# automatically add labels to form inputs.
#
# <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
# <%= f.text_field :first_name %>
@ -219,16 +241,23 @@ module ActionView
#
# <%= render :partial => f %>
#
# The rendered template is <tt>people/_labelling_form</tt> and the local variable referencing the form builder is called <tt>labelling_form</tt>.
# The rendered template is <tt>people/_labelling_form</tt> and the local
# variable referencing the form builder is called
# <tt>labelling_form</tt>.
#
# In many cases you will want to wrap the above in another helper, so you could do something like the following:
# The custom FormBuilder class is automatically merged with the options
# of a nested fields_for call, unless it's explicitely set.
#
# In many cases you will want to wrap the above in another helper, so you
# could do something like the following:
#
# def labelled_form_for(record_or_name_or_array, *args, &proc)
# options = args.extract_options!
# form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc)
# end
#
# If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag.
# If you don't need to attach a form to a model instance, then check out
# FormTagHelper#form_tag.
def form_for(record_or_name_or_array, *args, &proc)
raise ArgumentError, "Missing block" unless block_given?
@ -251,7 +280,7 @@ module ActionView
concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}))
fields_for(object_name, *(args << options), &proc)
concat('</form>')
concat('</form>'.html_safe!)
end
def apply_form_for_options!(object_or_array, options) #:nodoc:
@ -269,10 +298,12 @@ module ActionView
options[:url] ||= polymorphic_path(object_or_array)
end
# Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes
# fields_for suitable for specifying additional model objects in the same form:
# Creates a scope around a specific model object like form_for, but
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
#
# === Generic Examples
#
# ==== Examples
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# First name: <%= person_form.text_field :first_name %>
# Last name : <%= person_form.text_field :last_name %>
@ -282,20 +313,175 @@ module ActionView
# <% end %>
# <% end %>
#
# ...or if you have an object that needs to be represented as a different parameter, like a Client that acts as a Person:
# ...or if you have an object that needs to be represented as a different
# parameter, like a Client that acts as a Person:
#
# <% fields_for :person, @client do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# ...or if you don't have an object, just a name of the parameter
# ...or if you don't have an object, just a name of the parameter:
#
# <% fields_for :person do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base,
# like FormOptionHelper#collection_select and DateHelper#datetime_select.
# Note: This also works for the methods in FormOptionHelper and
# DateHelper that are designed to work with an object as base, like
# FormOptionHelper#collection_select and DateHelper#datetime_select.
#
# === Nested Attributes Examples
#
# When the object belonging to the current scope has a nested attribute
# writer for a certain attribute, fields_for will yield a new scope
# for that attribute. This allows you to create forms that set or change
# the attributes of a parent object and its associations in one go.
#
# Nested attribute writers are normal setter methods named after an
# association. The most common way of defining these writers is either
# with +accepts_nested_attributes_for+ in a model definition or by
# defining a method with the proper name. For example: the attribute
# writer for the association <tt>:address</tt> is called
# <tt>address_attributes=</tt>.
#
# Whether a one-to-one or one-to-many style form builder will be yielded
# depends on whether the normal reader method returns a _single_ object
# or an _array_ of objects.
#
# ==== One-to-one
#
# Consider a Person class which returns a _single_ Address from the
# <tt>address</tt> reader method and responds to the
# <tt>address_attributes=</tt> writer method:
#
# class Person
# def address
# @address
# end
#
# def address_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# This model can now be used with a nested fields_for, like so:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :address do |address_fields| %>
# Street : <%= address_fields.text_field :street %>
# Zip code: <%= address_fields.text_field :zip_code %>
# <% end %>
# <% end %>
#
# When address is already an association on a Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address
# end
#
# If you want to destroy the associated model through the form, you have
# to enable it first using the <tt>:allow_destroy</tt> option for
# +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address, :allow_destroy => true
# end
#
# Now, when you use a form element with the <tt>_delete</tt> parameter,
# with a value that evaluates to +true+, you will destroy the associated
# model (eg. 1, '1', true, or 'true'):
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :address do |address_fields| %>
# ...
# Delete: <%= address_fields.check_box :_delete %>
# <% end %>
# <% end %>
#
# ==== One-to-many
#
# Consider a Person class which returns an _array_ of Project instances
# from the <tt>projects</tt> reader method and responds to the
# <tt>projects_attributes=</tt> writer method:
#
# class Person
# def projects
# [@project1, @project2]
# end
#
# def projects_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# This model can now be used with a nested fields_for. The block given to
# the nested fields_for call will be repeated for each instance in the
# collection:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :projects do |project_fields| %>
# <% if project_fields.object.active? %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# <% end %>
#
# It's also possible to specify the instance to be used:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% @person.projects.each do |project| %>
# <% if project.active? %>
# <% person_form.fields_for :projects, project do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# <% end %>
# <% end %>
#
# Or a collection to be used:
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :projects, @active_projects do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
#
# When projects is already an association on Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects
# end
#
# If you want to destroy any of the associated models through the
# form, you have to enable it first using the <tt>:allow_destroy</tt>
# option for +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects, :allow_destroy => true
# end
#
# This will allow you to specify which models to destroy in the
# attributes hash by adding a form element for the <tt>_delete</tt>
# parameter with a value that evaluates to +true+
# (eg. 1, '1', true, or 'true'):
#
# <% form_for @person, :url => { :action => "update" } do |person_form| %>
# ...
# <% person_form.fields_for :projects do |project_fields| %>
# Delete: <%= project_fields.check_box :_delete %>
# <% end %>
# <% end %>
def fields_for(record_or_name_or_array, *args, &block)
raise ArgumentError, "Missing block" unless block_given?
options = args.extract_options!
@ -316,7 +502,8 @@ module ActionView
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify
# it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
# onto the HTML as an HTML element attribute as in the example shown.
# onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
# target labels for radio_button tags (where the value is used in the ID of the input tag).
#
# ==== Examples
# label(:post, :title)
@ -328,6 +515,9 @@ module ActionView
# label(:post, :title, "A short title", :class => "title_label")
# # => <label for="post_title" class="title_label">A short title</label>
#
# label(:post, :privacy, "Public Post", :value => "public")
# # => <label for="post_privacy_public">Public Post</label>
#
def label(object_name, method, text = nil, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options)
end
@ -498,8 +688,10 @@ module ActionView
# Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
# radio button will be checked. Additional options on the input tag can be passed as a
# hash with +options+.
# radio button will be checked.
#
# To force the radio button to be checked pass <tt>:checked => true</tt> in the
# +options+ hash. You may pass HTML options there as well.
#
# ==== Examples
# # Let's say that @post.category returns "rails":
@ -541,8 +733,10 @@ module ActionView
def to_label_tag(text = nil, options = {})
options = options.stringify_keys
tag_value = options.delete("value")
name_and_id = options.dup
add_default_name_and_id(name_and_id)
name_and_id["id"] = name_and_id["for"]
add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index")
options["for"] ||= name_and_id["id"]
content = (text.blank? ? nil : text.to_s) || method_name.humanize
@ -574,11 +768,7 @@ module ActionView
checked = self.class.radio_button_checked?(value(object), tag_value)
end
options["checked"] = "checked" if checked
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
options["id"] ||= defined?(@auto_index) ?
"#{tag_id_with_index(@auto_index)}_#{pretty_tag_value}" :
"#{tag_id}_#{pretty_tag_value}"
add_default_name_and_id(options)
add_default_name_and_id_for_value(tag_value, options)
tag("input", options)
end
@ -605,7 +795,9 @@ module ActionView
end
options["checked"] = "checked" if checked
add_default_name_and_id(options)
tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
hidden = tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value)
checkbox = tag("input", options)
(hidden + checkbox).html_safe!
end
def to_boolean_select_tag(options = {})
@ -677,6 +869,17 @@ module ActionView
end
private
def add_default_name_and_id_for_value(tag_value, options)
unless tag_value.nil?
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
specified_id = options["id"]
add_default_name_and_id(options)
options["id"] += "_#{pretty_tag_value}" unless specified_id
else
add_default_name_and_id(options)
end
end
def add_default_name_and_id(options)
if options.has_key?("index")
options["name"] ||= tag_name_with_index(options["index"])
@ -724,6 +927,7 @@ module ActionView
attr_accessor :object_name, :object, :options
def initialize(object_name, object, template, options, proc)
@nested_child_index = {}
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
@default_options = @options ? @options.slice(:index) : {}
if @object_name.to_s.match(/\[\]$/)
@ -735,11 +939,15 @@ module ActionView
end
end
(field_helpers - %w(label check_box radio_button fields_for)).each do |selector|
(field_helpers - %w(label check_box radio_button fields_for hidden_field)).each do |selector|
src = <<-end_src
def #{selector}(method, options = {})
@template.send(#{selector.inspect}, @object_name, method, objectify_options(options))
end
def #{selector}(method, options = {}) # def text_field(method, options = {})
@template.send( # @template.send(
#{selector.inspect}, # "text_field",
@object_name, # @object_name,
method, # method,
objectify_options(options)) # objectify_options(options))
end # end
end_src
class_eval src, __FILE__, __LINE__
end
@ -754,9 +962,18 @@ module ActionView
index = ""
end
if options[:builder]
args << {} unless args.last.is_a?(Hash)
args.last[:builder] ||= options[:builder]
end
case record_or_name_or_array
when String, Symbol
name = "#{object_name}#{index}[#{record_or_name_or_array}]"
if nested_attributes_association?(record_or_name_or_array)
return fields_for_with_nested_attributes(record_or_name_or_array, args, block)
else
name = "#{object_name}#{index}[#{record_or_name_or_array}]"
end
when Array
object = record_or_name_or_array.last
name = "#{object_name}#{index}[#{ActionController::RecordIdentifier.singular_class_name(object)}]"
@ -781,6 +998,11 @@ module ActionView
def radio_button(method, tag_value, options = {})
@template.radio_button(@object_name, method, tag_value, objectify_options(options))
end
def hidden_field(method, options = {})
@emitted_hidden_id = true if method == :id
@template.hidden_field(@object_name, method, objectify_options(options))
end
def error_message_on(method, *args)
@template.error_message_on(@object, method, *args)
@ -794,10 +1016,54 @@ module ActionView
@template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
end
def emitted_hidden_id?
@emitted_hidden_id
end
private
def objectify_options(options)
@default_options.merge(options.merge(:object => @object))
end
def nested_attributes_association?(association_name)
@object.respond_to?("#{association_name}_attributes=")
end
def fields_for_with_nested_attributes(association_name, args, block)
name = "#{object_name}[#{association_name}_attributes]"
association = args.first
if association.respond_to?(:new_record?)
association = [association] if @object.send(association_name).is_a?(Array)
elsif !association.is_a?(Array)
association = @object.send(association_name)
end
if association.is_a?(Array)
explicit_child_index = args.last[:child_index] if args.last.is_a?(Hash)
association.map do |child|
fields_for_nested_model("#{name}[#{explicit_child_index || nested_child_index(name)}]", child, args, block)
end.join
elsif association
fields_for_nested_model(name, association, args, block)
end
end
def fields_for_nested_model(name, object, args, block)
if object.new_record?
@template.fields_for(name, object, *args, &block)
else
@template.fields_for(name, object, *args) do |builder|
block.call(builder)
@template.concat builder.hidden_field(:id) unless builder.emitted_hidden_id?
end
end
end
def nested_child_index(name)
@nested_child_index[name] ||= -1
@nested_child_index[name] += 1
end
end
end

View file

@ -6,9 +6,7 @@ module ActionView
module Helpers
# Provides a number of methods for turning different kinds of containers into a set of option tags.
# == Options
# The <tt>collection_select</tt>, <tt>country_select</tt>, <tt>select</tt>,
# and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter,
# a hash.
# The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
#
# * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
#
@ -28,7 +26,7 @@ module ActionView
#
# Example with @post.person_id => 2:
#
# select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
#
# could become:
#
@ -43,7 +41,7 @@ module ActionView
#
# Example:
#
# select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
#
# could become:
#
@ -68,6 +66,36 @@ module ActionView
# <option value="rock">rock</option>
# <option value="country">country</option>
# </select>
#
# * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
#
# Example:
#
# select("post", "category", Post::CATEGORIES, {:disabled => 'restricted'})
#
# could become:
#
# <select name="post[category]">
# <option></option>
# <option>joke</option>
# <option>poem</option>
# <option disabled="disabled">restricted</option>
# </select>
#
# When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
#
# Example:
#
# collection_select(:post, :category_id, Category.all, :id, :name, {:disabled => lambda{|category| category.archived? }})
#
# If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
# <select name="post[category_id]">
# <option value="1" disabled="disabled">2008 stuff</option>
# <option value="2" disabled="disabled">Christmas</option>
# <option value="3">Jokes</option>
# <option value="4">Poems</option>
# </select>
#
module FormOptionsHelper
include ERB::Util
@ -76,7 +104,7 @@ module ActionView
# See options_for_select for the required format of the choices parameter.
#
# Example with @post.person_id => 1:
# select("post", "person_id", Person.find(:all).collect {|p| [ p.name, p.id ] }, { :include_blank => true })
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
#
# could become:
#
@ -94,7 +122,8 @@ module ActionView
# In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
#
# By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection
# or <tt>:selected => nil</tt> to leave all options unselected.
# or <tt>:selected => nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
# tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
def select(object, method, choices, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options)
end
@ -120,7 +149,7 @@ module ActionView
# end
#
# Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
# collection_select(:post, :author_id, Author.find(:all), :id, :name_with_initial, {:prompt => true})
# collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {:prompt => true})
#
# If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
# <select name="post[author_id]">
@ -133,6 +162,60 @@ module ActionView
InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end
# Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
# +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
# be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
# or <tt>:include_blank</tt> in the +options+ hash.
#
# Parameters:
# * +object+ - The instance of the class to be used for the select tag
# * +method+ - The attribute of +object+ corresponding to the select tag
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the <tt><option></tt> tags.
# * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
# string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
# * +option_value_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
#
# Example object structure for use with this method:
# class Continent < ActiveRecord::Base
# has_many :countries
# # attribs: id, name
# end
# class Country < ActiveRecord::Base
# belongs_to :continent
# # attribs: id, name, continent_id
# end
# class City < ActiveRecord::Base
# belongs_to :country
# # attribs: id, name, country_id
# end
#
# Sample usage:
# grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
#
# Possible output:
# <select name="city[country_id]">
# <optgroup label="Africa">
# <option value="1">South Africa</option>
# <option value="3">Somalia</option>
# </optgroup>
# <optgroup label="Europe">
# <option value="7" selected="selected">Denmark</option>
# <option value="2">Ireland</option>
# </optgroup>
# </select>
#
def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
end
# Return select and option tags for the given object and method, using
# #time_zone_options_for_select to generate the list of option tags.
#
@ -186,33 +269,70 @@ module ActionView
# options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
# <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option>
#
# If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
# or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
#
# Examples:
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum")
# <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"])
# <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced" disabled="disabled">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum")
# <option value="Free" selected="selected">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
#
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
def options_for_select(container, selected = nil)
return container if String === container
container = container.to_a if Hash === container
selected, disabled = extract_selected_and_disabled(selected)
options_for_select = container.inject([]) do |options, element|
text, value = option_text_and_value(element)
selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}>#{html_escape(text.to_s)}</option>)
disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled)
options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}>#{html_escape(text.to_s)}</option>)
end
options_for_select.join("\n")
options_for_select.join("\n").html_safe!
end
# Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
# the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
# If +selected+ is specified, the element returning a match on +value_method+ will get the selected option tag.
# Example:
# options_from_collection_for_select(@people, 'id', 'name')
# This will output the same HTML as if you did this:
# <option value="#{person.id}">#{person.name}</option>
#
# Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate an input tag:
# options_from_collection_for_select(@project.people, "id", "name")
# <option value="#{person.id}">#{person.name}</option>
# This is more often than not used inside a #select_tag like this example:
# select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
#
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
# If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
# will be selected option tag(s).
#
# If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
# function are the selected values.
#
# +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
#
# Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
# Failure to do this will produce undesired results. Example:
# options_from_collection_for_select(@people, 'id', 'name', '1')
# Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
# options_from_collection_for_select(@people, 'id', 'name', 1)
# should produce the desired results.
def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
options = collection.map do |element|
[element.send(text_method), element.send(value_method)]
end
options_for_select(options, selected)
selected, disabled = extract_selected_and_disabled(selected)
select_deselect = {}
select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected)
select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled)
options_for_select(options, select_deselect)
end
# Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
@ -230,7 +350,8 @@ module ActionView
# +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
# * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
# which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
# to +option_key_method+. If +nil+, no selection is made.
# to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
# to be specified.
#
# Example object structure for use with this method:
# class Continent < ActiveRecord::Base
@ -269,6 +390,62 @@ module ActionView
end
end
# Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
# wraps them with <tt><optgroup></tt> tags.
#
# Parameters:
# * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
# <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
# nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
# Ex. ["North America",[["United States","US"],["Canada","CA"]]]
# * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
# which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
# as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
# * +prompt+ - set to true or a prompt string. When the select element doesnt have a value yet, this
# prepends an option with a generic prompt — "Please select" — or the given prompt string.
#
# Sample usage (Array):
# grouped_options = [
# ['North America',
# [['United States','US'],'Canada']],
# ['Europe',
# ['Denmark','Germany','France']]
# ]
# grouped_options_for_select(grouped_options)
#
# Sample usage (Hash):
# grouped_options = {
# 'North America' => [['United States','US], 'Canada'],
# 'Europe' => ['Denmark','Germany','France']
# }
# grouped_options_for_select(grouped_options)
#
# Possible output:
# <optgroup label="Europe">
# <option value="Denmark">Denmark</option>
# <option value="Germany">Germany</option>
# <option value="France">France</option>
# </optgroup>
# <optgroup label="North America">
# <option value="US">United States</option>
# <option value="Canada">Canada</option>
# </optgroup>
#
# <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
# wrap the output in an appropriate <tt><select></tt> tag.
def grouped_options_for_select(grouped_options, selected_key = nil, prompt = nil)
body = ''
body << content_tag(:option, prompt, :value => "") if prompt
grouped_options = grouped_options.sort if grouped_options.is_a?(Hash)
grouped_options.each do |group|
body << content_tag(:optgroup, options_for_select(group[1], selected_key), :label => group[0])
end
body
end
# Returns a string of option tags for pretty much any time zone in the
# world. Supply a TimeZone name as +selected+ to have it marked as the
# selected option tag. You can also supply an array of TimeZone objects
@ -324,6 +501,24 @@ module ActionView
value == selected
end
end
def extract_selected_and_disabled(selected)
if selected.is_a?(Hash)
[selected[:selected], selected[:disabled]]
else
[selected, nil]
end
end
def extract_values_from_collection(collection, value_method, selected)
if selected.is_a?(Proc)
collection.map do |element|
element.send(value_method) if selected.call(element)
end.compact
else
selected
end
end
end
class InstanceTag #:nodoc:
@ -334,15 +529,27 @@ module ActionView
add_default_name_and_id(html_options)
value = value(object)
selected_value = options.has_key?(:selected) ? options[:selected] : value
content_tag("select", add_options(options_for_select(choices, selected_value), options, selected_value), html_options)
disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil
content_tag("select", add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options)
end
def to_collection_select_tag(collection, value_method, text_method, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil
selected_value = options.has_key?(:selected) ? options[:selected] : value
content_tag(
"select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options
"select", add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options
)
end
def to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
value = value(object)
content_tag(
"select", add_options(option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value), options, value), html_options
)
end
@ -364,7 +571,8 @@ module ActionView
option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
end
if value.blank? && options[:prompt]
("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags
prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('support.select.prompt', :default => 'Please select')
"<option value=\"\">#{prompt}</option>\n" + option_tags
else
option_tags
end
@ -380,6 +588,10 @@ module ActionView
@template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
end
def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
@template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options))
end
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
end

View file

@ -230,6 +230,8 @@ module ActionView
# * <tt>:rows</tt> - Specify the number of rows in the textarea
# * <tt>:cols</tt> - Specify the number of columns in the textarea
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped.
# If you need unescaped contents, set this to false.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
@ -257,7 +259,10 @@ module ActionView
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys)
escape = options.key?("escape") ? options.delete("escape") : true
content = html_escape(content) if escape
content_tag :textarea, content, { "name" => name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end
# Creates a check box form input tag.
@ -353,15 +358,16 @@ module ActionView
disable_with << ";#{options.delete('onclick')}" if options['onclick']
options["onclick"] = "if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }"
options["onclick"] << "else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }"
options["onclick"] << "else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';"
options["onclick"] << "hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }"
options["onclick"] << "this.setAttribute('originalValue', this.value);this.disabled = true;#{disable_with};"
options["onclick"] << "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());"
options["onclick"] << "if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;"
end
if confirm = options.delete("confirm")
options["onclick"] ||= ''
options["onclick"] << "return #{confirm_javascript_function(confirm)};"
options["onclick"] ||= 'return true;'
options["onclick"] = "if (!#{confirm_javascript_function(confirm)}) return false; #{options['onclick']}"
end
tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys)
@ -426,7 +432,7 @@ module ActionView
concat(tag(:fieldset, options, true))
concat(content_tag(:legend, legend)) unless legend.blank?
concat(content)
concat("</fieldset>")
concat("</fieldset>".html_safe!)
end
private
@ -444,23 +450,23 @@ module ActionView
''
when /^post$/i, "", nil
html_options["method"] = "post"
protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0') : ''
protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0;display:inline') : ''
else
html_options["method"] = "post"
content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0')
content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0;display:inline')
end
end
def form_tag_html(html_options)
extra_tags = extra_tags_for_form(html_options)
tag(:form, html_options, true) + extra_tags
(tag(:form, html_options, true) + extra_tags).html_safe!
end
def form_tag_in_block(html_options, &block)
content = capture(&block)
concat(form_tag_html(html_options))
concat(content)
concat("</form>")
concat("</form>".html_safe!)
end
def token_tag

View file

@ -15,6 +15,7 @@ module ActionView
# * <tt>:country_code</tt> - Sets the country code for the phone number.
#
# ==== Examples
# number_to_phone(5551234) # => 555-1234
# number_to_phone(1235551234) # => 123-555-1234
# number_to_phone(1235551234, :area_code => true) # => (123) 555-1234
# number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234
@ -37,7 +38,8 @@ module ActionView
str << if area_code
number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3")
else
number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
number.starts_with?('-') ? number.slice!(1..-1) : number
end
str << " x #{extension}" unless extension.blank?
str
@ -138,7 +140,7 @@ module ActionView
# number_with_delimiter(12345678) # => 12,345,678
# number_with_delimiter(12345678.05) # => 12,345,678.05
# number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678
# number_with_delimiter(12345678, :seperator => ",") # => 12,345,678
# number_with_delimiter(12345678, :separator => ",") # => 12,345,678
# number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
# # => 98 765 432,98
#
@ -220,7 +222,7 @@ module ActionView
end
end
STORAGE_UNITS = %w( Bytes KB MB GB TB ).freeze
STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze
# Formats the bytes in +size+ into a more understandable representation
# (e.g., giving it 1500 yields 1.5 KB). This method is useful for
@ -244,12 +246,17 @@ module ActionView
# number_to_human_size(483989, :precision => 0) # => 473 KB
# number_to_human_size(1234567, :precision => 2, :separator => ',') # => 1,18 MB
#
# Zeros after the decimal point are always stripped out, regardless of the
# specified precision:
# helper.number_to_human_size(1234567890123, :precision => 5) # => "1.12283 TB"
# helper.number_to_human_size(524288000, :precision=>5) # => "500 MB"
#
# You can still use <tt>number_to_human_size</tt> with the old API that accepts the
# +precision+ as its optional second parameter:
# number_to_human_size(1234567, 2) # => 1.18 MB
# number_to_human_size(483989, 0) # => 473 KB
def number_to_human_size(number, *args)
return number.nil? ? nil : pluralize(number.to_i, "Byte") if number.to_i < 1024
return nil if number.nil?
options = args.extract_options!
options.symbolize_keys!
@ -268,22 +275,32 @@ module ActionView
separator ||= (options[:separator] || defaults[:separator])
delimiter ||= (options[:delimiter] || defaults[:delimiter])
max_exp = STORAGE_UNITS.size - 1
number = Float(number)
exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
number /= 1024 ** exponent
unit = STORAGE_UNITS[exponent]
storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true)
begin
escaped_separator = Regexp.escape(separator)
number_with_precision(number,
:precision => precision,
:separator => separator,
:delimiter => delimiter
).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}"
rescue
number
if number.to_i < 1024
unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true)
storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
else
max_exp = STORAGE_UNITS.size - 1
number = Float(number)
exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
number /= 1024 ** exponent
unit_key = STORAGE_UNITS[exponent]
unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true)
begin
escaped_separator = Regexp.escape(separator)
formatted_number = number_with_precision(number,
:precision => precision,
:separator => separator,
:delimiter => delimiter
).sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit)
rescue
number
end
end
end
end

View file

@ -1,4 +1,5 @@
require 'set'
require 'active_support/json'
module ActionView
module Helpers
@ -106,7 +107,7 @@ module ActionView
# on the page in an Ajax response.
module PrototypeHelper
unless const_defined? :CALLBACKS
CALLBACKS = Set.new([ :uninitialized, :loading, :loaded,
CALLBACKS = Set.new([ :create, :uninitialized, :loading, :loaded,
:interactive, :complete, :failure, :success ] +
(100..599).to_a)
AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url,
@ -392,7 +393,7 @@ module ActionView
concat(form_remote_tag(options))
fields_for(object_name, *(args << options), &proc)
concat('</form>')
concat('</form>'.html_safe!)
end
alias_method :form_remote_for, :remote_form_for
@ -530,11 +531,6 @@ module ActionView
# is shorthand for
# :with => "'name=' + value"
# This essentially just changes the key of the parameter.
# <tt>:on</tt>:: Specifies which event handler to observe. By default,
# it's set to "changed" for text fields and areas and
# "click" for radio buttons and checkboxes. With this,
# you can specify it instead to be "blur" or "focus" or
# any other event.
#
# Additionally, you may specify any of the options documented in the
# <em>Common options</em> section at the top of this document.
@ -547,11 +543,6 @@ module ActionView
# :url => 'http://example.com/books/edit/1',
# :with => 'title'
#
# # Sends params: {:book_title => 'Title of the book'} when the focus leaves
# # the input field.
# observe_field 'book_title',
# :url => 'http://example.com/books/edit/1',
# :on => 'blur'
#
def observe_field(field_id, options = {})
if options[:frequency] && options[:frequency] > 0
@ -695,7 +686,7 @@ module ActionView
# Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
# expression as an argument to another JavaScriptGenerator method.
def literal(code)
ActiveSupport::JSON::Variable.new(code.to_s)
::ActiveSupport::JSON::Variable.new(code.to_s)
end
# Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
@ -982,7 +973,7 @@ module ActionView
def loop_on_multiple_args(method, ids)
record(ids.size>1 ?
"#{javascript_object_for(ids)}.each(#{method})" :
"#{method}(#{ids.first.to_json})")
"#{method}(#{::ActiveSupport::JSON.encode(ids.first)})")
end
def page
@ -1006,7 +997,7 @@ module ActionView
end
def javascript_object_for(object)
object.respond_to?(:to_json) ? object.to_json : object.inspect
::ActiveSupport::JSON.encode(object)
end
def arguments_for_call(arguments, block = nil)
@ -1093,7 +1084,6 @@ module ActionView
javascript << "#{options[:frequency]}, " if options[:frequency]
javascript << "function(element, value) {"
javascript << "#{callback}}"
javascript << ", '#{options[:on]}'" if options[:on]
javascript << ")"
javascript_tag(javascript)
end
@ -1149,7 +1139,7 @@ module ActionView
class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
def initialize(generator, id)
@id = id
super(generator, "$(#{id.to_json})")
super(generator, "$(#{::ActiveSupport::JSON.encode(id)})")
end
# Allows access of element attributes through +attribute+. Examples:
@ -1221,7 +1211,7 @@ module ActionView
enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
else
add_variable_assignment!(variable)
append_enumerable_function!("eachSlice(#{number.to_json});")
append_enumerable_function!("eachSlice(#{::ActiveSupport::JSON.encode(number)});")
end
end
@ -1242,7 +1232,7 @@ module ActionView
def pluck(variable, property)
add_variable_assignment!(variable)
append_enumerable_function!("pluck(#{property.to_json});")
append_enumerable_function!("pluck(#{::ActiveSupport::JSON.encode(property)});")
end
def zip(variable, *arguments, &block)
@ -1306,7 +1296,7 @@ module ActionView
class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
def initialize(generator, pattern)
super(generator, "$$(#{pattern.to_json})")
super(generator, "$$(#{::ActiveSupport::JSON.encode(pattern)})")
end
end
end

View file

@ -0,0 +1,9 @@
module ActionView #:nodoc:
module Helpers #:nodoc:
module RawOutputHelper
def raw(stringish)
stringish.to_s.html_safe!
end
end
end
end

View file

@ -1,15 +1,5 @@
require 'action_view/helpers/tag_helper'
begin
require 'html/document'
rescue LoadError
html_scanner_path = "#{File.dirname(__FILE__)}/../../action_controller/vendor/html-scanner"
if File.directory?(html_scanner_path)
$:.unshift html_scanner_path
require 'html/document'
end
end
module ActionView
module Helpers #:nodoc:
# The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
@ -59,7 +49,11 @@ module ActionView
# confuse browsers.
#
def sanitize(html, options = {})
self.class.white_list_sanitizer.sanitize(html, options)
returning self.class.white_list_sanitizer.sanitize(html, options) do |sanitized|
if sanitized
sanitized.html_safe!
end
end
end
# Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
@ -82,7 +76,11 @@ module ActionView
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => Welcome to my website!
def strip_tags(html)
self.class.full_sanitizer.sanitize(html)
returning self.class.full_sanitizer.sanitize(html) do |sanitized|
if sanitized
sanitized.html_safe!
end
end
end
# Strips all link tags from +text+ leaving just the link text.

View file

@ -1,4 +1,5 @@
require 'action_view/helpers/javascript_helper'
require 'active_support/json'
module ActionView
module Helpers
@ -42,7 +43,7 @@ module ActionView
# You can change the behaviour with various options, see
# http://script.aculo.us for more documentation.
def visual_effect(name, element_id = false, js_options = {})
element = element_id ? element_id.to_json : "element"
element = element_id ? ActiveSupport::JSON.encode(element_id) : "element"
js_options[:queue] = if js_options[:queue].is_a?(Hash)
'{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
@ -137,7 +138,7 @@ module ActionView
end
def sortable_element_js(element_id, options = {}) #:nodoc:
options[:with] ||= "Sortable.serialize(#{element_id.to_json})"
options[:with] ||= "Sortable.serialize(#{ActiveSupport::JSON.encode(element_id)})"
options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) }
@ -148,7 +149,7 @@ module ActionView
options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment]
options[:only] = array_or_string_for_javascript(options[:only]) if options[:only]
%(Sortable.create(#{element_id.to_json}, #{options_for_javascript(options)});)
%(Sortable.create(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end
# Makes the element with the DOM ID specified by +element_id+ draggable.
@ -163,7 +164,7 @@ module ActionView
end
def draggable_element_js(element_id, options = {}) #:nodoc:
%(new Draggable(#{element_id.to_json}, #{options_for_javascript(options)});)
%(new Draggable(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end
# Makes the element with the DOM ID specified by +element_id+ receive
@ -218,7 +219,7 @@ module ActionView
# Confirmation happens during the onDrop callback, so it can be removed from the options
options.delete(:confirm) if options[:confirm]
%(Droppables.add(#{element_id.to_json}, #{options_for_javascript(options)});)
%(Droppables.add(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end
end
end

View file

@ -1,5 +1,4 @@
require 'cgi'
require 'erb'
require 'action_view/erb/util'
require 'set'
module ActionView
@ -39,7 +38,7 @@ module ActionView
# tag("img", { :src => "open &amp; shut.png" }, false, false)
# # => <img src="open &amp; shut.png" />
def tag(name, options = nil, open = false, escape = true)
"<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}"
"<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe!
end
# Returns an HTML block tag of type +name+ surrounding the +content+. Add
@ -92,19 +91,19 @@ module ActionView
# cdata_section(File.read("hello_world.txt"))
# # => <![CDATA[<hello from a text file]]>
def cdata_section(content)
"<![CDATA[#{content}]]>"
"<![CDATA[#{content}]]>".html_safe!
end
# Returns an escaped version of +html+ without affecting existing escaped entities.
#
# ==== Examples
# escape_once("1 > 2 &amp; 3")
# escape_once("1 < 2 &amp; 3")
# # => "1 &lt; 2 &amp; 3"
#
# escape_once("&lt;&lt; Accept & Checkout")
# # => "&lt;&lt; Accept &amp; Checkout"
def escape_once(html)
html.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
ActiveSupport::Multibyte.clean(html.to_s).gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] }
end
private
@ -126,7 +125,7 @@ module ActionView
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
"<#{name}#{tag_options}>#{content}</#{name}>"
"<#{name}#{tag_options}>#{content}</#{name}>".html_safe!
end
def tag_options(options, escape = true)
@ -143,7 +142,7 @@ module ActionView
else
attrs = options.map { |key, value| %(#{key}="#{value}") }
end
" #{attrs.sort * ' '}" unless attrs.empty?
" #{attrs.sort * ' '}".html_safe! unless attrs.empty?
end
end
end

View file

@ -1,15 +1,5 @@
require 'action_view/helpers/tag_helper'
begin
require 'html/document'
rescue LoadError
html_scanner_path = "#{File.dirname(__FILE__)}/../../action_controller/vendor/html-scanner"
if File.directory?(html_scanner_path)
$:.unshift html_scanner_path
require 'html/document'
end
end
module ActionView
module Helpers #:nodoc:
# The TextHelper module provides a set of methods for filtering, formatting
@ -43,30 +33,31 @@ module ActionView
end
# Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
# (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...").
# (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
# for a total length not exceeding <tt>:length</tt>.
#
# ==== Examples
#
# truncate("Once upon a time in a world far far away")
# # => Once upon a time in a world f...
# # => Once upon a time in a world...
#
# truncate("Once upon a time in a world far far away", :length => 14)
# # => Once upon a...
#
# truncate("And they found that many people were sleeping better.", :length => 25, "(clipped)")
# # => And they found that many (clipped)
# # => And they found t(clipped)
#
# truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 15)
# # => And they found... (continued)
# truncate("And they found that many people were sleeping better.", :omission => "... (continued)", :length => 25)
# # => And they f... (continued)
#
# You can still use <tt>truncate</tt> with the old API that accepts the
# +length+ as its optional second and the +ellipsis+ as its
# optional third parameter:
# truncate("Once upon a time in a world far far away", 14)
# # => Once upon a time in a world f...
# # => Once upon a...
#
# truncate("And they found that many people were sleeping better.", 15, "... (continued)")
# # => And they found... (continued)
# truncate("And they found that many people were sleeping better.", 25, "... (continued)")
# # => And they f... (continued)
def truncate(text, *args)
options = args.extract_options!
unless args.empty?
@ -117,7 +108,7 @@ module ActionView
text
else
match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
text.gsub(/(#{match})/i, options[:highlighter])
text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter])
end
end
@ -226,91 +217,87 @@ module ActionView
end * "\n"
end
begin
require_library_or_gem "redcloth" unless Object.const_defined?(:RedCloth)
# Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags.
#
# You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
# <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
# is available</i>.
#
# ==== Examples
# textilize("*This is Textile!* Rejoice!")
# # => "<p><strong>This is Textile!</strong> Rejoice!</p>"
#
# textilize("I _love_ ROR(Ruby on Rails)!")
# # => "<p>I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!</p>"
#
# textilize("h2. Textile makes markup -easy- simple!")
# # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
#
# textilize("Visit the Rails website "here":http://www.rubyonrails.org/.)
# # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>"
#
# textilize("This is worded <strong>strongly</strong>")
# # => "<p>This is worded <strong>strongly</strong></p>"
#
# textilize("This is worded <strong>strongly</strong>", :filter_html)
# # => "<p>This is worded &lt;strong&gt;strongly&lt;/strong&gt;</p>"
#
def textilize(text, *options)
options ||= [:hard_breaks]
# Returns the text with all the Textile[http://www.textism.com/tools/textile] codes turned into HTML tags.
#
# You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
# <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
# is available</i>.
#
# ==== Examples
# textilize("*This is Textile!* Rejoice!")
# # => "<p><strong>This is Textile!</strong> Rejoice!</p>"
#
# textilize("I _love_ ROR(Ruby on Rails)!")
# # => "<p>I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!</p>"
#
# textilize("h2. Textile makes markup -easy- simple!")
# # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
#
# textilize("Visit the Rails website "here":http://www.rubyonrails.org/.)
# # => "<p>Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>.</p>"
def textilize(text)
if text.blank?
""
else
textilized = RedCloth.new(text, [ :hard_breaks ])
textilized.hard_breaks = true if textilized.respond_to?(:hard_breaks=)
textilized.to_html
end
if text.blank?
""
else
textilized = RedCloth.new(text, options)
textilized.to_html
end
# Returns the text with all the Textile codes turned into HTML tags,
# but without the bounding <p> tag that RedCloth adds.
#
# You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
# <i>This method is only available if RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
# is available</i>.
#
# ==== Examples
# textilize_without_paragraph("*This is Textile!* Rejoice!")
# # => "<strong>This is Textile!</strong> Rejoice!"
#
# textilize_without_paragraph("I _love_ ROR(Ruby on Rails)!")
# # => "I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!"
#
# textilize_without_paragraph("h2. Textile makes markup -easy- simple!")
# # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
#
# textilize_without_paragraph("Visit the Rails website "here":http://www.rubyonrails.org/.)
# # => "Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>."
def textilize_without_paragraph(text)
textiled = textilize(text)
if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
return textiled
end
rescue LoadError
# We can't really help what's not there
end
begin
require_library_or_gem "bluecloth" unless Object.const_defined?(:BlueCloth)
# Returns the text with all the Textile codes turned into HTML tags,
# but without the bounding <p> tag that RedCloth adds.
#
# You can learn more about Textile's syntax at its website[http://www.textism.com/tools/textile].
# <i>This method is requires RedCloth[http://whytheluckystiff.net/ruby/redcloth/]
# to be available</i>.
#
# ==== Examples
# textilize_without_paragraph("*This is Textile!* Rejoice!")
# # => "<strong>This is Textile!</strong> Rejoice!"
#
# textilize_without_paragraph("I _love_ ROR(Ruby on Rails)!")
# # => "I <em>love</em> <acronym title="Ruby on Rails">ROR</acronym>!"
#
# textilize_without_paragraph("h2. Textile makes markup -easy- simple!")
# # => "<h2>Textile makes markup <del>easy</del> simple!</h2>"
#
# textilize_without_paragraph("Visit the Rails website "here":http://www.rubyonrails.org/.)
# # => "Visit the Rails website <a href="http://www.rubyonrails.org/">here</a>."
def textilize_without_paragraph(text)
textiled = textilize(text)
if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
return textiled
end
# Returns the text with all the Markdown codes turned into HTML tags.
# <i>This method is only available if BlueCloth[http://www.deveiate.org/projects/BlueCloth]
# is available</i>.
#
# ==== Examples
# markdown("We are using __Markdown__ now!")
# # => "<p>We are using <strong>Markdown</strong> now!</p>"
#
# markdown("We like to _write_ `code`, not just _read_ it!")
# # => "<p>We like to <em>write</em> <code>code</code>, not just <em>read</em> it!</p>"
#
# markdown("The [Markdown website](http://daringfireball.net/projects/markdown/) has more information.")
# # => "<p>The <a href="http://daringfireball.net/projects/markdown/">Markdown website</a>
# # has more information.</p>"
#
# markdown('![The ROR logo](http://rubyonrails.com/images/rails.png "Ruby on Rails")')
# # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>'
def markdown(text)
text.blank? ? "" : BlueCloth.new(text).to_html
end
rescue LoadError
# We can't really help what's not there
# Returns the text with all the Markdown codes turned into HTML tags.
# <i>This method requires BlueCloth[http://www.deveiate.org/projects/BlueCloth] or another
# Markdown library to be installed.</i>.
#
# ==== Examples
# markdown("We are using __Markdown__ now!")
# # => "<p>We are using <strong>Markdown</strong> now!</p>"
#
# markdown("We like to _write_ `code`, not just _read_ it!")
# # => "<p>We like to <em>write</em> <code>code</code>, not just <em>read</em> it!</p>"
#
# markdown("The [Markdown website](http://daringfireball.net/projects/markdown/) has more information.")
# # => "<p>The <a href="http://daringfireball.net/projects/markdown/">Markdown website</a>
# # has more information.</p>"
#
# markdown('![The ROR logo](http://rubyonrails.com/images/rails.png "Ruby on Rails")')
# # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>'
def markdown(text)
text.blank? ? "" : Markdown.new(text).to_html
end
# Returns +text+ transformed into HTML using simple formatting rules.
@ -392,8 +379,8 @@ module ActionView
options.reverse_merge!(:link => :all, :html => {})
case options[:link].to_sym
when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), &block)
when :email_addresses then auto_link_email_addresses(text, &block)
when :all then auto_link_email_addresses(auto_link_urls(text, options[:html], &block), options[:html], &block)
when :email_addresses then auto_link_email_addresses(text, options[:html], &block)
when :urls then auto_link_urls(text, options[:html], &block)
end
end
@ -545,45 +532,44 @@ module ActionView
end
AUTO_LINK_RE = %r{
( # leading text
<\w+.*?>| # leading HTML tag, or
[^=!:'"/]| # leading punctuation, or
^ # beginning of line
)
(
(?:https?://)| # protocol spec, or
(?:www\.) # www.*
)
(
[-\w]+ # subdomain or domain
(?:\.[-\w]+)* # remaining subdomains or domain
(?::\d+)? # port
(?:/(?:[~\w\+@%=\(\)-]|(?:[,.;:'][^\s$]))*)* # path
(?:\?[\w\+@%&=.;:-]+)? # query string
(?:\#[\w\-]*)? # trailing anchor
)
([[:punct:]]|<|$|) # trailing text
}x unless const_defined?(:AUTO_LINK_RE)
( https?:// | www\. )
[^\s<]+
}x unless const_defined?(:AUTO_LINK_RE)
BRACKETS = { ']' => '[', ')' => '(', '}' => '{' }
# Turns all urls into clickable links. If a block is given, each url
# is yielded and the result is used as the link text.
def auto_link_urls(text, html_options = {})
extra_options = tag_options(html_options.stringify_keys) || ""
link_attributes = html_options.stringify_keys
text.gsub(AUTO_LINK_RE) do
all, a, b, c, d = $&, $1, $2, $3, $4
if a =~ /<a\s/i # don't replace URL's that are already linked
all
href = $&
punctuation = ''
left, right = $`, $'
# detect already linked URLs and URLs in the middle of a tag
if left =~ /<[^>]+$/ && right =~ /^[^>]*>/
# do not change string; URL is alreay linked
href
else
text = b + c
text = yield(text) if block_given?
%(#{a}<a href="#{b=="www."?"http://www.":b}#{c}"#{extra_options}>#{text}</a>#{d})
# don't include trailing punctuation character as part of the URL
if href.sub!(/[^\w\/-]$/, '') and punctuation = $& and opening = BRACKETS[punctuation]
if href.scan(opening).size > href.scan(punctuation).size
href << punctuation
punctuation = ''
end
end
link_text = block_given?? yield(href) : href
href = 'http://' + href unless href.index('http') == 0
content_tag(:a, h(link_text), link_attributes.merge('href' => href)) + punctuation
end
end
end
# Turns all email addresses into clickable links. If a block is given,
# each email is yielded and the result is used as the link text.
def auto_link_email_addresses(text)
def auto_link_email_addresses(text, html_options = {})
body = text.dup
text.gsub(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
text = $1
@ -592,7 +578,7 @@ module ActionView
text
else
display_text = (block_given?) ? yield(text) : text
%{<a href="mailto:#{text}">#{display_text}</a>}
mail_to text, display_text, html_options
end
end
end

View file

@ -3,19 +3,37 @@ require 'action_view/helpers/tag_helper'
module ActionView
module Helpers
module TranslationHelper
# Delegates to I18n#translate but also performs two additional functions. First, it'll catch MissingTranslationData exceptions
# and turn them into inline spans that contains the missing key, such that you can see in a view what is missing where.
#
# Second, it'll scope the key by the current partial if the key starts with a period. So if you call translate(".foo") from the
# people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo"). This makes it less repetitive
# to translate many keys within the same partials and gives you a simple framework for scoping them consistently. If you don't
# prepend the key with a period, nothing is converted.
def translate(key, options = {})
options[:raise] = true
I18n.translate(key, options)
I18n.translate(scope_key_by_partial(key), options)
rescue I18n::MissingTranslationData => e
keys = I18n.send(:normalize_translation_keys, e.locale, e.key, e.options[:scope])
content_tag('span', keys.join(', '), :class => 'translation_missing')
end
alias :t :translate
# Delegates to I18n.localize with no additional functionality.
def localize(*args)
I18n.localize *args
I18n.localize(*args)
end
alias :l :localize
private
def scope_key_by_partial(key)
if key.to_s.first == "."
template.path_without_format_and_extension.gsub(%r{/_?}, ".") + key.to_s
else
key
end
end
end
end
end

View file

@ -1,4 +1,4 @@
require 'action_view/helpers/javascript_helper'
#require 'action_view/helpers/javascript_helper'
module ActionView
module Helpers #:nodoc:
@ -219,7 +219,7 @@ module ActionView
if block_given?
options = args.first || {}
html_options = args.second
concat(link_to(capture(&block), options, html_options))
concat(link_to(capture(&block), options, html_options).html_safe!)
else
name = args.first
options = args.second || {}
@ -237,7 +237,7 @@ module ActionView
end
href_attr = "href=\"#{url}\"" unless href
"<a #{href_attr}#{tag_options}>#{name || url}</a>"
"<a #{href_attr}#{tag_options}>#{name || url}</a>".html_safe!
end
end
@ -309,7 +309,7 @@ module ActionView
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + tag("input", html_options) + request_token_tag + "</div></form>"
method_tag + tag("input", html_options) + request_token_tag + "</div></form>".html_safe!
end
@ -507,7 +507,30 @@ module ActionView
# current_page?(:controller => 'shop', :action => 'checkout')
# # => true
#
# current_page?(:controller => 'shop', :action => 'checkout', :order => 'asc)
# current_page?(:controller => 'shop', :action => 'checkout', :order => 'asc')
# # => false
#
# current_page?(:action => 'checkout')
# # => true
#
# current_page?(:controller => 'library', :action => 'checkout')
# # => false
#
# Let's say we're in the <tt>/shop/checkout?order=desc&page=1</tt> action.
#
# current_page?(:action => 'process')
# # => false
#
# current_page?(:controller => 'shop', :action => 'checkout')
# # => true
#
# current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page=>'1')
# # => true
#
# current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc', :page=>'2')
# # => false
#
# current_page?(:controller => 'shop', :action => 'checkout', :order => 'desc')
# # => false
#
# current_page?(:action => 'checkout')
@ -516,7 +539,7 @@ module ActionView
# current_page?(:controller => 'library', :action => 'checkout')
# # => false
def current_page?(options)
url_string = CGI.escapeHTML(url_for(options))
url_string = CGI.unescapeHTML(url_for(options))
request = @controller.request
# We ignore any extra parameters in the request_uri if the
# submitted url doesn't have any either. This lets the function
@ -545,7 +568,7 @@ module ActionView
when confirm && popup
"if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
when confirm && method
"if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;"
"if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method, url, href)} };return false;"
when confirm
"return #{confirm_javascript_function(confirm)};"
when method

View file

@ -12,7 +12,7 @@ module ActionView #:nodoc:
private
# Always recompile inline templates
def recompile?(local_assigns)
def recompile?
true
end
end

View file

@ -44,6 +44,18 @@
# separator:
delimiter: ""
precision: 1
storage_units:
# Storage units output formatting.
# %u is the storage unit, %n is the number (default: 2 MB)
format: "%n %u"
units:
byte:
one: "Byte"
other: "Bytes"
kb: "KB"
mb: "MB"
gb: "GB"
tb: "TB"
# Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words()
datetime:
@ -79,6 +91,16 @@
over_x_years:
one: "over 1 year"
other: "over {{count}} years"
almost_x_years:
one: "almost 1 year"
other: "almost {{count}} years"
prompts:
year: "Year"
month: "Month"
day: "Day"
hour: "Hour"
minute: "Minute"
second: "Seconds"
activerecord:
errors:
@ -89,3 +111,7 @@
# The variable :count is also available
body: "There were problems with the following fields:"
support:
select:
# default value for :prompt => true in FormOptionsHelper
prompt: "Please select"

View file

@ -46,6 +46,38 @@ module ActionView
#
# This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from.
#
# == Rendering objects with the RecordIdentifier
#
# Instead of explicitly naming the location of a partial, you can also let the RecordIdentifier do the work if
# you're following its conventions for RecordIdentifier#partial_path. Examples:
#
# # @account is an Account instance, so it uses the RecordIdentifier to replace
# # <%= render :partial => "accounts/account", :locals => { :account => @buyer } %>
# <%= render :partial => @account %>
#
# # @posts is an array of Post instances, so it uses the RecordIdentifier to replace
# # <%= render :partial => "posts/post", :collection => @posts %>
# <%= render :partial => @posts %>
#
# == Rendering the default case
#
# If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
# defaults of render to render partials. Examples:
#
# # Instead of <%= render :partial => "account" %>
# <%= render "account" %>
#
# # Instead of <%= render :partial => "account", :locals => { :account => @buyer } %>
# <%= render "account", :account => @buyer %>
#
# # @account is an Account instance, so it uses the RecordIdentifier to replace
# # <%= render :partial => "accounts/account", :locals => { :account => @account } %>
# <%= render(@account) %>
#
# # @posts is an array of Post instances, so it uses the RecordIdentifier to replace
# # <%= render :partial => "posts/post", :collection => @posts %>
# <%= render(@posts) %>
#
# == Rendering partials with layouts
#
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
@ -155,15 +187,20 @@ module ActionView
builder_partial_path = partial_path.class.to_s.demodulize.underscore.sub(/_builder$/, '')
local_assigns.merge!(builder_partial_path.to_sym => partial_path)
render_partial(:partial => builder_partial_path, :object => options[:object], :locals => local_assigns)
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope
render_partial_collection(options.except(:partial).merge(:collection => partial_path))
else
object = partial_path
render_partial(
:partial => ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path),
:object => object,
:locals => local_assigns
)
if Array === partial_path ||
(defined?(ActiveRecord) &&
(ActiveRecord::Associations::AssociationCollection === partial_path ||
ActiveRecord::NamedScope::Scope === partial_path))
render_partial_collection(options.except(:partial).merge(:collection => partial_path))
else
object = partial_path
render_partial(
:partial => ActionController::RecordIdentifier.partial_path(object, controller.class.controller_path),
:object => object,
:locals => local_assigns
)
end
end
end
@ -184,7 +221,7 @@ module ActionView
result = template.render_partial(self, object, local_assigns.dup, as)
index += 1
result
end.join(spacer)
end.join(spacer).html_safe!
end
def _pick_partial_template(partial_path) #:nodoc:
@ -196,7 +233,7 @@ module ActionView
path = "_#{partial_path}"
end
_pick_template(path)
self.view_paths.find_template(path, self.template_format)
end
memoize :_pick_partial_template
end

Some files were not shown because too many files have changed in this diff Show more