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,3 +1,24 @@
*2.3.5 (November 25, 2009)*
* Minor Bug Fixes and deprecation warnings
*2.3.4 (September 4, 2009)*
* Minor bug fixes.
*2.3.3 (July 12, 2009)*
* No changes, just a version bump.
*2.3.2 [Final] (March 15, 2009)*
* Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones]
* Fixed RFC-2045 quoted-printable bug #1421 [squadette]
* Fixed that no body charset would be set when there are attachments present #740 [Paweł Kondzior]
*2.2.1 [RC2] (November 14th, 2008)* *2.2.1 [RC2] (November 14th, 2008)*
* Turn on STARTTLS if it is available in Net::SMTP (added in Ruby 1.8.7) and the SMTP server supports it (This is required for Gmail's SMTP server) #1336 [Grant Hollingworth] * Turn on STARTTLS if it is available in Net::SMTP (added in Ruby 1.8.7) and the SMTP server supports it (This is required for Gmail's SMTP server) #1336 [Grant Hollingworth]
@ -5,7 +26,7 @@
*2.2.0 [RC1] (October 24th, 2008)* *2.2.0 [RC1] (October 24th, 2008)*
* Add layout functionality to mailers [Pratik] * Add layout functionality to mailers [Pratik Naik]
Mailer layouts behaves just like controller layouts, except layout names need to Mailer layouts behaves just like controller layouts, except layout names need to
have '_mailer' postfix for them to be automatically picked up. have '_mailer' postfix for them to be automatically picked up.
@ -17,7 +38,7 @@
* Less verbose mail logging: just recipients for :info log level; the whole email for :debug only. #8000 [iaddict, Tarmo Tänav] * Less verbose mail logging: just recipients for :info log level; the whole email for :debug only. #8000 [iaddict, Tarmo Tänav]
* Updated TMail to version 1.2.1 [raasdnil] * Updated TMail to version 1.2.1 [Mikel Lindsaar]
* Fixed that you don't have to call super in ActionMailer::TestCase#setup #10406 [jamesgolick] * Fixed that you don't have to call super in ActionMailer::TestCase#setup #10406 [jamesgolick]
@ -29,7 +50,7 @@
*2.0.1* (December 7th, 2007) *2.0.1* (December 7th, 2007)
* Update ActionMailer so it treats ActionView the same way that ActionController does. Closes #10244 [rick] * Update ActionMailer so it treats ActionView the same way that ActionController does. Closes #10244 [Rick Olson]
* Pass the template_root as an array as ActionView's view_path * Pass the template_root as an array as ActionView's view_path
* Request templates with the "#{mailer_name}/#{action}" as opposed to just "#{action}" * Request templates with the "#{mailer_name}/#{action}" as opposed to just "#{action}"
@ -38,11 +59,11 @@
* Update README to use new smtp settings configuration API. Closes #10060 [psq] * Update README to use new smtp settings configuration API. Closes #10060 [psq]
* Allow ActionMailer subclasses to individually set their delivery method (so two subclasses can have different delivery methods) #10033 [zdennis] * Allow ActionMailer subclasses to individually set their delivery method (so two subclasses can have different delivery methods) #10033 [Zach Dennis]
* Update TMail to v1.1.0. Use an updated version of TMail if available. [mikel] * Update TMail to v1.1.0. Use an updated version of TMail if available. [Mikel Lindsaar]
* Introduce a new base test class for testing Mailers. ActionMailer::TestCase [Koz] * Introduce a new base test class for testing Mailers. ActionMailer::TestCase [Michael Koziarski]
* Fix silent failure of rxml templates. #9879 [jstewart] * Fix silent failure of rxml templates. #9879 [jstewart]
@ -77,7 +98,7 @@
*1.3.2* (February 5th, 2007) *1.3.2* (February 5th, 2007)
* Deprecate server_settings renaming it to smtp_settings, add sendmail_settings to allow you to override the arguments to and location of the sendmail executable. [Koz] * Deprecate server_settings renaming it to smtp_settings, add sendmail_settings to allow you to override the arguments to and location of the sendmail executable. [Michael Koziarski]
*1.3.1* (January 16th, 2007) *1.3.1* (January 16th, 2007)
@ -97,7 +118,7 @@
* Tighten rescue clauses. #5985 [james@grayproductions.net] * Tighten rescue clauses. #5985 [james@grayproductions.net]
* Automatically included ActionController::UrlWriter, such that URL generation can happen within ActionMailer controllers. [DHH] * Automatically included ActionController::UrlWriter, such that URL generation can happen within ActionMailer controllers. [David Heinemeier Hansson]
* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar] * Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar]

View file

@ -1,4 +1,4 @@
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 Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View file

@ -4,7 +4,6 @@ require 'rake/testtask'
require 'rake/rdoctask' require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -55,7 +54,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.2.2' + PKG_BUILD) s.add_dependency('actionpack', '= 2.3.5' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
@ -76,12 +75,14 @@ end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
require 'rake/contrib/sshpublisher'
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'` `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
require 'rake/contrib/sshpublisher'
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload
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 # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the
@ -31,22 +31,32 @@ rescue LoadError
end end
end end
require 'action_mailer/vendor' require 'action_view'
require 'tmail'
require 'action_mailer/base' module ActionMailer
require 'action_mailer/helpers' def self.load_all!
require 'action_mailer/mail_helper' [Base, Part, ::Text::Format, ::Net::SMTP]
require 'action_mailer/quoting'
require 'action_mailer/test_helper'
require 'net/smtp'
ActionMailer::Base.class_eval do
include ActionMailer::Quoting
include ActionMailer::Helpers
helper MailHelper
end end
silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) } autoload :AdvAttrAccessor, 'action_mailer/adv_attr_accessor'
autoload :Base, 'action_mailer/base'
autoload :Helpers, 'action_mailer/helpers'
autoload :Part, 'action_mailer/part'
autoload :PartContainer, 'action_mailer/part_container'
autoload :Quoting, 'action_mailer/quoting'
autoload :TestCase, 'action_mailer/test_case'
autoload :TestHelper, 'action_mailer/test_helper'
autoload :Utils, 'action_mailer/utils'
end
module Text
autoload :Format, 'action_mailer/vendor/text_format'
end
module Net
autoload :SMTP, 'net/smtp'
end
autoload :MailHelper, 'action_mailer/mail_helper'
require 'action_mailer/vendor/tmail'

View file

@ -1,9 +1,3 @@
require 'action_mailer/adv_attr_accessor'
require 'action_mailer/part'
require 'action_mailer/part_container'
require 'action_mailer/utils'
require 'tmail/net'
module ActionMailer #:nodoc: module ActionMailer #:nodoc:
# Action Mailer allows you to send email from your application using a mailer model and views. # Action Mailer allows you to send email from your application using a mailer model and views.
# #
@ -23,6 +17,7 @@ module ActionMailer #:nodoc:
# class Notifier < ActionMailer::Base # class Notifier < ActionMailer::Base
# def signup_notification(recipient) # def signup_notification(recipient)
# recipients recipient.email_address_with_name # recipients recipient.email_address_with_name
# bcc ["bcc@example.com", "Order Watcher <watcher@example.com>"]
# from "system@example.com" # from "system@example.com"
# subject "New account information" # subject "New account information"
# body :account => recipient # body :account => recipient
@ -218,6 +213,8 @@ module ActionMailer #:nodoc:
# * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting. # * <tt>:password</tt> - If your mail server requires authentication, set the password in this setting.
# * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here. # * <tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here.
# This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>. # This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>.
# * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server and starts to use it.
# It works only on Ruby >= 1.8.7 and Ruby >= 1.9. Default is true.
# #
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method. # * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>. # * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
@ -235,17 +232,20 @@ module ActionMailer #:nodoc:
# #
# * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
# pick a different charset from inside a method with +charset+. # pick a different charset from inside a method with +charset+.
#
# * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You # * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
# can also pick a different content type from inside a method with +content_type+. # can also pick a different content type from inside a method with +content_type+.
#
# * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to <tt>1.0</tt>. You # * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to <tt>1.0</tt>. You
# can also pick a different value from inside a method with +mime_version+. # can also pick a different value from inside a method with +mime_version+.
#
# * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates # * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
# which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to # which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to
# <tt>["text/html", "text/enriched", "text/plain"]</tt>. Items that appear first in the array have higher priority in the mail client # <tt>["text/html", "text/enriched", "text/plain"]</tt>. Items that appear first in the array have higher priority in the mail client
# and appear last in the mime encoded message. You can also pick a different order from inside a method with # and appear last in the mime encoded message. You can also pick a different order from inside a method with
# +implicit_parts_order+. # +implicit_parts_order+.
class Base class Base
include AdvAttrAccessor, PartContainer include AdvAttrAccessor, PartContainer, Quoting, Utils
if Object.const_defined?(:ActionController) if Object.const_defined?(:ActionController)
include ActionController::UrlWriter include ActionController::UrlWriter
include ActionController::Layout include ActionController::Layout
@ -254,6 +254,8 @@ module ActionMailer #:nodoc:
private_class_method :new #:nodoc: private_class_method :new #:nodoc:
class_inheritable_accessor :view_paths class_inheritable_accessor :view_paths
self.view_paths = []
cattr_accessor :logger cattr_accessor :logger
@@smtp_settings = { @@smtp_settings = {
@ -262,7 +264,8 @@ module ActionMailer #:nodoc:
:domain => 'localhost.localdomain', :domain => 'localhost.localdomain',
:user_name => nil, :user_name => nil,
:password => nil, :password => nil,
:authentication => nil :authentication => nil,
:enable_starttls_auto => true,
} }
cattr_accessor :smtp_settings cattr_accessor :smtp_settings
@ -426,12 +429,6 @@ module ActionMailer #:nodoc:
new.deliver!(mail) new.deliver!(mail)
end end
def register_template_extension(extension)
ActiveSupport::Deprecation.warn(
"ActionMailer::Base.register_template_extension has been deprecated." +
"Use ActionView::Base.register_template_extension instead", caller)
end
def template_root def template_root
self.view_paths && self.view_paths.first self.view_paths && self.view_paths.first
end end
@ -482,7 +479,7 @@ module ActionMailer #:nodoc:
) )
end end
unless @parts.empty? unless @parts.empty?
@content_type = "multipart/alternative" @content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order) @parts = sort_parts(@parts, @implicit_parts_order)
end end
end end
@ -546,10 +543,16 @@ module ActionMailer #:nodoc:
@headers ||= {} @headers ||= {}
@body ||= {} @body ||= {}
@mime_version = @@default_mime_version.dup if @@default_mime_version @mime_version = @@default_mime_version.dup if @@default_mime_version
@sent_on ||= Time.now
end end
def render_message(method_name, body) def render_message(method_name, body)
if method_name.respond_to?(:content_type)
@current_template_content_type = method_name.content_type
end
render :file => method_name, :body => body render :file => method_name, :body => body
ensure
@current_template_content_type = nil
end end
def render(opts) def render(opts)
@ -568,11 +571,17 @@ module ActionMailer #:nodoc:
end end
def default_template_format def default_template_format
if @current_template_content_type
Mime::Type.lookup(@current_template_content_type).to_sym
else
:html :html
end end
end
def candidate_for_layout?(options) def candidate_for_layout?(options)
!@template.send(:_exempt_from_layout?, default_template_name) !self.view_paths.find_template(default_template_name, default_template_format).exempt_from_layout?
rescue ActionView::MissingTemplate
return true
end end
def template_root def template_root
@ -588,7 +597,9 @@ module ActionMailer #:nodoc:
end end
def initialize_template_class(assigns) def initialize_template_class(assigns)
ActionView::Base.new(view_paths, assigns, self) template = ActionView::Base.new(self.class.view_paths, assigns, self)
template.template_format = default_template_format
template
end end
def sort_parts(parts, order = []) def sort_parts(parts, order = [])
@ -637,11 +648,11 @@ module ActionMailer #:nodoc:
if @parts.empty? if @parts.empty?
m.set_content_type(real_content_type, nil, ctype_attrs) m.set_content_type(real_content_type, nil, ctype_attrs)
m.body = Utils.normalize_new_lines(body) m.body = normalize_new_lines(body)
else else
if String === body if String === body
part = TMail::Mail.new part = TMail::Mail.new
part.body = Utils.normalize_new_lines(body) part.body = normalize_new_lines(body)
part.set_content_type(real_content_type, nil, ctype_attrs) part.set_content_type(real_content_type, nil, ctype_attrs)
part.set_content_disposition "inline" part.set_content_disposition "inline"
m.parts << part m.parts << part
@ -664,10 +675,10 @@ module ActionMailer #:nodoc:
def perform_delivery_smtp(mail) def perform_delivery_smtp(mail)
destinations = mail.destinations destinations = mail.destinations
mail.ready_to_send mail.ready_to_send
sender = mail['return-path'] || mail.from sender = (mail['return-path'] && mail['return-path'].spec) || mail['from']
smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port]) smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
smtp.enable_starttls_auto if smtp.respond_to?(:enable_starttls_auto) smtp.enable_starttls_auto if smtp_settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
smtp.start(smtp_settings[:domain], smtp_settings[:user_name], smtp_settings[:password], smtp.start(smtp_settings[:domain], smtp_settings[:user_name], smtp_settings[:password],
smtp_settings[:authentication]) do |smtp| smtp_settings[:authentication]) do |smtp|
smtp.sendmail(mail.encoded, sender, destinations) smtp.sendmail(mail.encoded, sender, destinations)
@ -687,4 +698,9 @@ module ActionMailer #:nodoc:
deliveries << mail deliveries << mail
end end
end end
Base.class_eval do
include Helpers
helper MailHelper
end
end end

View file

@ -1,3 +1,5 @@
require 'active_support/dependencies'
module ActionMailer module ActionMailer
module Helpers #:nodoc: module Helpers #:nodoc:
def self.included(base) #:nodoc: def self.included(base) #:nodoc:

View file

@ -1,5 +1,3 @@
require 'text/format'
module MailHelper module MailHelper
# Uses Text::Format to take the text and format it, indented two spaces for # Uses Text::Format to take the text and format it, indented two spaces for
# each line, and wrapped at 72 columns. # each line, and wrapped at 72 columns.

View file

@ -1,15 +1,10 @@
require 'action_mailer/adv_attr_accessor'
require 'action_mailer/part_container'
require 'action_mailer/utils'
module ActionMailer module ActionMailer
# Represents a subpart of an email message. It shares many similar # Represents a subpart of an email message. It shares many similar
# attributes of ActionMailer::Base. Although you can create parts manually # attributes of ActionMailer::Base. Although you can create parts manually
# and add them to the +parts+ list of the mailer, it is easier # and add them to the +parts+ list of the mailer, it is easier
# to use the helper methods in ActionMailer::PartContainer. # to use the helper methods in ActionMailer::PartContainer.
class Part class Part
include ActionMailer::AdvAttrAccessor include AdvAttrAccessor, PartContainer, Utils
include ActionMailer::PartContainer
# Represents the body of the part, as a string. This should not be a # Represents the body of the part, as a string. This should not be a
# Hash (like ActionMailer::Base), but if you want a template to be rendered # Hash (like ActionMailer::Base), but if you want a template to be rendered
@ -64,7 +59,7 @@ module ActionMailer
when "base64" then when "base64" then
part.body = TMail::Base64.folding_encode(body) part.body = TMail::Base64.folding_encode(body)
when "quoted-printable" when "quoted-printable"
part.body = [Utils.normalize_new_lines(body)].pack("M*") part.body = [normalize_new_lines(body)].pack("M*")
else else
part.body = body part.body = body
end end
@ -93,7 +88,10 @@ module ActionMailer
part.parts << prt part.parts << prt
end end
part.set_content_type(real_content_type, nil, ctype_attrs) if real_content_type =~ /multipart/ if real_content_type =~ /multipart/
ctype_attrs.delete 'charset'
part.set_content_type(real_content_type, nil, ctype_attrs)
end
end end
headers.each { |k,v| part[k] = v } headers.each { |k,v| part[k] = v }
@ -102,7 +100,6 @@ module ActionMailer
end end
private private
def squish(values={}) def squish(values={})
values.delete_if { |k,v| v.nil? } values.delete_if { |k,v| v.nil? }
end end

View file

@ -41,7 +41,11 @@ module ActionMailer
private private
def parse_content_type(defaults=nil) def parse_content_type(defaults=nil)
return [defaults && defaults.content_type, {}] if content_type.blank? if content_type.blank?
return defaults ?
[ defaults.content_type, { 'charset' => defaults.charset } ] :
[ nil, {} ]
end
ctype, *attrs = content_type.split(/;\s*/) ctype, *attrs = content_type.split(/;\s*/)
attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h } attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h }
[ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)] [ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)]

View file

@ -12,7 +12,7 @@ module ActionMailer
# account multi-byte characters (if executing with $KCODE="u", for instance) # account multi-byte characters (if executing with $KCODE="u", for instance)
def quoted_printable_encode(character) def quoted_printable_encode(character)
result = "" result = ""
character.each_byte { |b| result << "=%02x" % b } character.each_byte { |b| result << "=%02X" % b }
result result
end end

View file

@ -10,7 +10,7 @@ module ActionMailer
end end
class TestCase < ActiveSupport::TestCase class TestCase < ActiveSupport::TestCase
include ActionMailer::Quoting include Quoting, TestHelper
setup :initialize_test_deliveries setup :initialize_test_deliveries
setup :set_expected_mail setup :set_expected_mail

View file

@ -58,6 +58,7 @@ module ActionMailer
end end
end end
# TODO: Deprecate this
module Test module Test
module Unit module Unit
class TestCase class TestCase

View file

@ -3,6 +3,5 @@ module ActionMailer
def normalize_new_lines(text) def normalize_new_lines(text)
text.to_s.gsub(/\r\n?/, "\n") text.to_s.gsub(/\r\n?/, "\n")
end end
module_function :normalize_new_lines
end end
end end

View file

@ -1,14 +0,0 @@
# Prefer gems to the bundled libs.
require 'rubygems'
begin
gem 'tmail', '~> 1.2.3'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/tmail-1.2.3"
end
begin
gem 'text-format', '>= 0.6.3'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/vendor/text-format-0.6.3"
end

View file

@ -1150,7 +1150,7 @@ if __FILE__ == $0
assert_equal(Text::Format::JUSTIFY, @format_o.format_style) assert_equal(Text::Format::JUSTIFY, @format_o.format_style)
assert_match(/^of freedom, and that government of the people, by the people, for the$/, assert_match(/^of freedom, and that government of the people, by the people, for the$/,
@format_o.format(GETTYSBURG).split("\n")[-3]) @format_o.format(GETTYSBURG).split("\n")[-3])
assert_raises(ArgumentError) { @format_o.format_style = 33 } assert_raise(ArgumentError) { @format_o.format_style = 33 }
end end
def test_tag_paragraph def test_tag_paragraph

View file

@ -0,0 +1,10 @@
# Prefer gems to the bundled libs.
require 'rubygems'
begin
gem 'text-format', '>= 0.6.3'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/text-format-0.6.3"
end
require 'text/format'

View file

@ -43,6 +43,7 @@ module Racc
class Parser class Parser
old_verbose, $VERBOSE = $VERBOSE, nil
Racc_Runtime_Version = '1.4.5' Racc_Runtime_Version = '1.4.5'
Racc_Runtime_Revision = '$Revision: 1.7 $'.split[1] Racc_Runtime_Revision = '$Revision: 1.7 $'.split[1]
@ -71,6 +72,7 @@ module Racc
Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R
Racc_Runtime_Type = 'ruby' Racc_Runtime_Type = 'ruby'
end end
$VERBOSE = old_verbose
def Parser.racc_runtime_type def Parser.racc_runtime_type
Racc_Runtime_Type Racc_Runtime_Type

View file

@ -0,0 +1,17 @@
# Prefer gems to the bundled libs.
require 'rubygems'
begin
gem 'tmail', '~> 1.2.3'
rescue Gem::LoadError
$:.unshift "#{File.dirname(__FILE__)}/tmail-1.2.3"
end
module TMail
end
require 'tmail'
silence_warnings do
TMail::Encoder.const_set("MAX_LINE_LEN", 200)
end

View file

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

View file

@ -1 +1,2 @@
require 'action_mailer' require 'action_mailer'
ActiveSupport::Deprecation.warn 'require "actionmailer" is deprecated and will be removed in Rails 3. Use require "action_mailer" instead.'

View file

@ -1,3 +1,4 @@
require 'rubygems'
require 'test/unit' require 'test/unit'
$:.unshift "#{File.dirname(__FILE__)}/../lib" $:.unshift "#{File.dirname(__FILE__)}/../lib"
@ -9,8 +10,15 @@ require 'action_mailer/test_case'
# Show backtraces for deprecated behavior for quicker cleanup. # Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true ActiveSupport::Deprecation.debug = true
# Bogus template processors
ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect }
ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect }
$:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers" $:.unshift "#{File.dirname(__FILE__)}/fixtures/helpers"
ActionMailer::Base.template_root = "#{File.dirname(__FILE__)}/fixtures"
ActionView::Base.cache_template_loading = true
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
ActionMailer::Base.template_root = FIXTURE_LOAD_PATH
class MockSMTP class MockSMTP
def self.deliveries def self.deliveries
@ -37,7 +45,6 @@ class Net::SMTP
end end
def uses_gem(gem_name, test_name, version = '> 0') def uses_gem(gem_name, test_name, version = '> 0')
require 'rubygems'
gem gem_name.to_s, version gem gem_name.to_s, version
require gem_name.to_s require gem_name.to_s
yield yield
@ -45,13 +52,6 @@ rescue LoadError
$stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again." $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
end end
# Wrap tests that use Mocha and skip if unavailable.
unless defined? uses_mocha
def uses_mocha(test_name, &block)
uses_gem('mocha', test_name, '>= 0.5.5', &block)
end
end
def set_delivery_method(delivery_method) def set_delivery_method(delivery_method)
@old_delivery_method = ActionMailer::Base.delivery_method @old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = delivery_method ActionMailer::Base.delivery_method = delivery_method

View file

@ -0,0 +1,54 @@
require 'abstract_unit'
class AssetHostMailer < ActionMailer::Base
def email_with_asset(recipient)
recipients recipient
subject "testing email containing asset path while asset_host is set"
from "tester@example.com"
end
end
class AssetHostTest < Test::Unit::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def teardown
restore_delivery_method
end
def test_asset_host_as_string
ActionController::Base.asset_host = "http://www.example.com"
mail = AssetHostMailer.deliver_email_with_asset(@recipient)
assert_equal "<img alt=\"Somelogo\" src=\"http://www.example.com/images/somelogo.png\" />", mail.body.strip
end
def test_asset_host_as_one_arguement_proc
ActionController::Base.asset_host = Proc.new { |source|
if source.starts_with?('/images')
"http://images.example.com"
else
"http://assets.example.com"
end
}
mail = AssetHostMailer.deliver_email_with_asset(@recipient)
assert_equal "<img alt=\"Somelogo\" src=\"http://images.example.com/images/somelogo.png\" />", mail.body.strip
end
def test_asset_host_as_two_arguement_proc
ActionController::Base.asset_host = Proc.new {|source,request|
if request && request.ssl?
"https://www.example.com"
else
"http://www.example.com"
end
}
mail = nil
assert_nothing_raised { mail = AssetHostMailer.deliver_email_with_asset(@recipient) }
assert_equal "<img alt=\"Somelogo\" src=\"http://www.example.com/images/somelogo.png\" />", mail.body.strip
end
end

View file

@ -0,0 +1 @@
<%= image_tag "somelogo.png" %>

View file

@ -0,0 +1 @@
text/html multipart

View file

@ -0,0 +1 @@
text/plain multipart

View file

@ -1,5 +1,5 @@
module ExampleHelper module ExampleHelper
def example_format(text) def example_format(text)
"<em><strong><small>#{text}</small></strong></em>" "<em><strong><small>#{h(text)}</small></strong></em>".html_safe!
end end
end end

View file

@ -0,0 +1 @@
text/plain layout - <%= yield %>

View file

@ -20,6 +20,14 @@ class AutoLayoutMailer < ActionMailer::Base
from "tester@example.com" from "tester@example.com"
body render(:inline => "Hello, <%= @world %>", :layout => false, :body => { :world => "Earth" }) body render(:inline => "Hello, <%= @world %>", :layout => false, :body => { :world => "Earth" })
end end
def multipart(recipient, type = nil)
recipients recipient
subject "You have a mail"
from "tester@example.com"
content_type(type) if type
end
end end
class ExplicitLayoutMailer < ActionMailer::Base class ExplicitLayoutMailer < ActionMailer::Base
@ -56,6 +64,43 @@ class LayoutMailerTest < Test::Unit::TestCase
assert_equal "Hello from layout Inside", mail.body.strip assert_equal "Hello from layout Inside", mail.body.strip
end end
def test_should_pickup_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient)
assert_equal "multipart/alternative", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
def test_should_pickup_multipartmixed_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "multipart/mixed")
assert_equal "multipart/mixed", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
def test_should_fix_multipart_layout
mail = AutoLayoutMailer.create_multipart(@recipient, "text/plain")
assert_equal "multipart/alternative", mail.content_type
assert_equal 2, mail.parts.size
assert_equal 'text/plain', mail.parts.first.content_type
assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body
assert_equal 'text/html', mail.parts.last.content_type
assert_equal "Hello from layout text/html multipart", mail.parts.last.body
end
def test_should_pickup_layout_given_to_render def test_should_pickup_layout_given_to_render
mail = AutoLayoutMailer.create_spam(@recipient) mail = AutoLayoutMailer.create_spam(@recipient)
assert_equal "Spammer layout Hello, Earth", mail.body.strip assert_equal "Spammer layout Hello, Earth", mail.body.strip

View file

@ -18,7 +18,6 @@ class TestMailer < ActionMailer::Base
@recipients = recipient @recipients = recipient
@subject = "[Signed up] Welcome #{recipient}" @subject = "[Signed up] Welcome #{recipient}"
@from = "system@loudthinking.com" @from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient @body["recipient"] = recipient
end end
@ -289,8 +288,6 @@ class TestMailer < ActionMailer::Base
end end
end end
uses_mocha 'ActionMailerTest' do
class ActionMailerTest < Test::Unit::TestCase class ActionMailerTest < Test::Unit::TestCase
include ActionMailer::Quoting include ActionMailer::Quoting
@ -332,6 +329,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_equal "multipart/mixed", created.content_type assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "bar", created.parts.first.header['foo'].to_s assert_equal "bar", created.parts.first.header['foo'].to_s
assert_nil created.parts.first.charset
assert_equal "text/plain", created.parts.first.parts.first.content_type assert_equal "text/plain", created.parts.first.parts.first.content_type
assert_equal "text/html", created.parts.first.parts[1].content_type assert_equal "text/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type assert_equal "application/octet-stream", created.parts[1].content_type
@ -357,12 +355,14 @@ class ActionMailerTest < Test::Unit::TestCase
end end
def test_signed_up def test_signed_up
Time.stubs(:now => Time.now)
expected = new_mail expected = new_mail
expected.to = @recipient expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}" expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}" expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com" expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12) expected.date = Time.now
created = nil created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) } assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
@ -389,6 +389,8 @@ class ActionMailerTest < Test::Unit::TestCase
end end
def test_custom_templating_extension def test_custom_templating_extension
assert ActionView::Template.template_handler_extensions.include?("haml"), "haml extension was not registered"
# N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory # N.b., custom_templating_extension.text.plain.haml is expected to be in fixtures/test_mailer directory
expected = new_mail expected = new_mail
expected.to = @recipient expected.to = @recipient
@ -568,7 +570,9 @@ class ActionMailerTest < Test::Unit::TestCase
mail = TestMailer.create_signed_up(@recipient) mail = TestMailer.create_signed_up(@recipient)
logger = mock() logger = mock()
logger.expects(:info).with("Sent mail to #{@recipient}") logger.expects(:info).with("Sent mail to #{@recipient}")
logger.expects(:debug).with("\n#{mail.encoded}") logger.expects(:debug).with() do |logged_text|
logged_text =~ /\[Signed up\] Welcome/
end
TestMailer.logger = logger TestMailer.logger = logger
TestMailer.deliver_signed_up(@recipient) TestMailer.deliver_signed_up(@recipient)
end end
@ -799,6 +803,8 @@ EOF
end end
def test_implicitly_multipart_messages def test_implicitly_multipart_messages
assert ActionView::Template.template_handler_extensions.include?("bak"), "bak extension was not registered"
mail = TestMailer.create_implicitly_multipart_example(@recipient) mail = TestMailer.create_implicitly_multipart_example(@recipient)
assert_equal 3, mail.parts.length assert_equal 3, mail.parts.length
assert_equal "1.0", mail.mime_version assert_equal "1.0", mail.mime_version
@ -812,6 +818,8 @@ EOF
end end
def test_implicitly_multipart_messages_with_custom_order def test_implicitly_multipart_messages_with_custom_order
assert ActionView::Template.template_handler_extensions.include?("bak"), "bak extension was not registered"
mail = TestMailer.create_implicitly_multipart_example(@recipient, nil, ["text/yaml", "text/plain"]) mail = TestMailer.create_implicitly_multipart_example(@recipient, nil, ["text/yaml", "text/plain"])
assert_equal 3, mail.parts.length assert_equal 3, mail.parts.length
assert_equal "text/html", mail.parts[0].content_type assert_equal "text/html", mail.parts[0].content_type
@ -915,6 +923,8 @@ EOF
def test_multipart_with_template_path_with_dots def test_multipart_with_template_path_with_dots
mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient) mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient)
assert_equal 2, mail.parts.length assert_equal 2, mail.parts.length
assert_equal 'text/plain', mail.parts[0].content_type
assert_equal 'utf-8', mail.parts[0].charset
end end
def test_custom_content_type_attributes def test_custom_content_type_attributes
@ -932,6 +942,7 @@ EOF
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_return_path TestMailer.deliver_return_path
assert_match %r{^Return-Path: <another@somewhere.test>}, MockSMTP.deliveries[0][0] assert_match %r{^Return-Path: <another@somewhere.test>}, MockSMTP.deliveries[0][0]
assert_equal "another@somewhere.test", MockSMTP.deliveries[0][1].to_s
end end
def test_body_is_stored_as_an_ivar def test_body_is_stored_as_an_ivar
@ -940,6 +951,7 @@ EOF
end end
def test_starttls_is_enabled_if_supported def test_starttls_is_enabled_if_supported
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
MockSMTP.any_instance.expects(:respond_to?).with(:enable_starttls_auto).returns(true) MockSMTP.any_instance.expects(:respond_to?).with(:enable_starttls_auto).returns(true)
MockSMTP.any_instance.expects(:enable_starttls_auto) MockSMTP.any_instance.expects(:enable_starttls_auto)
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.delivery_method = :smtp
@ -947,25 +959,34 @@ EOF
end end
def test_starttls_is_disabled_if_not_supported def test_starttls_is_disabled_if_not_supported
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
MockSMTP.any_instance.expects(:respond_to?).with(:enable_starttls_auto).returns(false) MockSMTP.any_instance.expects(:respond_to?).with(:enable_starttls_auto).returns(false)
MockSMTP.any_instance.expects(:enable_starttls_auto).never MockSMTP.any_instance.expects(:enable_starttls_auto).never
ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_signed_up(@recipient) TestMailer.deliver_signed_up(@recipient)
end end
end
end # uses_mocha def test_starttls_is_not_enabled
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = false
MockSMTP.any_instance.expects(:respond_to?).never
MockSMTP.any_instance.expects(:enable_starttls_auto).never
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_signed_up(@recipient)
ensure
ActionMailer::Base.smtp_settings[:enable_starttls_auto] = true
end
end
class InheritableTemplateRootTest < Test::Unit::TestCase class InheritableTemplateRootTest < Test::Unit::TestCase
def test_attr def test_attr
expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots" expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
assert_equal expected, FunkyPathMailer.template_root assert_equal expected, FunkyPathMailer.template_root.to_s
sub = Class.new(FunkyPathMailer) sub = Class.new(FunkyPathMailer)
sub.template_root = 'test/path' sub.template_root = 'test/path'
assert_equal 'test/path', sub.template_root assert_equal 'test/path', sub.template_root.to_s
assert_equal expected, FunkyPathMailer.template_root assert_equal expected, FunkyPathMailer.template_root.to_s
end end
end end
@ -1051,7 +1072,7 @@ class RespondToTest < Test::Unit::TestCase
end end
def test_should_still_raise_exception_with_expected_message_when_calling_an_undefined_method def test_should_still_raise_exception_with_expected_message_when_calling_an_undefined_method
error = assert_raises NoMethodError do error = assert_raise NoMethodError do
RespondToMailer.not_a_method RespondToMailer.not_a_method
end end

View file

@ -1,6 +1,5 @@
# encoding: utf-8 # encoding: utf-8
require 'abstract_unit' require 'abstract_unit'
require 'tmail'
require 'tempfile' require 'tempfile'
class QuotingTest < Test::Unit::TestCase class QuotingTest < Test::Unit::TestCase
@ -49,8 +48,10 @@ class QuotingTest < Test::Unit::TestCase
result = execute_in_sandbox(<<-CODE) result = execute_in_sandbox(<<-CODE)
$:.unshift(File.dirname(__FILE__) + "/../lib/") $:.unshift(File.dirname(__FILE__) + "/../lib/")
if RUBY_VERSION < '1.9'
$KCODE = 'u' $KCODE = 'u'
require 'jcode' require 'jcode'
end
require 'action_mailer/quoting' require 'action_mailer/quoting'
include ActionMailer::Quoting include ActionMailer::Quoting
quoted_printable(#{original.inspect}, "UTF-8") quoted_printable(#{original.inspect}, "UTF-8")

View file

@ -26,7 +26,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end end
def test_determine_default_mailer_raises_correct_error def test_determine_default_mailer_raises_correct_error
assert_raises(ActionMailer::NonInferrableMailerError) do assert_raise(ActionMailer::NonInferrableMailerError) do
self.class.determine_default_mailer("NotAMailerTest") self.class.determine_default_mailer("NotAMailerTest")
end end
end end
@ -36,7 +36,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end end
def test_encode def test_encode
assert_equal "=?utf-8?Q?=0aasdf=0a?=", encode("\nasdf\n") assert_equal "=?utf-8?Q?=0Aasdf=0A?=", encode("\nasdf\n")
end end
def test_assert_emails def test_assert_emails
@ -84,7 +84,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end end
def test_assert_emails_too_few_sent def test_assert_emails_too_few_sent
error = assert_raises Test::Unit::AssertionFailedError do error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do assert_emails 2 do
TestHelperMailer.deliver_test TestHelperMailer.deliver_test
end end
@ -94,7 +94,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end end
def test_assert_emails_too_many_sent def test_assert_emails_too_many_sent
error = assert_raises Test::Unit::AssertionFailedError do error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 1 do assert_emails 1 do
TestHelperMailer.deliver_test TestHelperMailer.deliver_test
TestHelperMailer.deliver_test TestHelperMailer.deliver_test
@ -105,7 +105,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end end
def test_assert_no_emails_failure def test_assert_no_emails_failure
error = assert_raises Test::Unit::AssertionFailedError do error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do assert_no_emails do
TestHelperMailer.deliver_test TestHelperMailer.deliver_test
end end

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
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 Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

View file

@ -10,7 +10,7 @@ Action Pack implements these actions as public methods on Action Controllers
and uses Action Views to implement the template rendering. Action Controllers and uses Action Views to implement the template rendering. Action Controllers
are then responsible for handling all the actions relating to a certain part are then responsible for handling all the actions relating to a certain part
of an application. This grouping usually consists of actions for lists and for of an application. This grouping usually consists of actions for lists and for
CRUDs revolving around a single (or a few) model objects. So ContactController CRUDs revolving around a single (or a few) model objects. So ContactsController
would be responsible for listing contacts, creating, deleting, and updating would be responsible for listing contacts, creating, deleting, and updating
contacts. A WeblogController could be responsible for both posts and comments. contacts. A WeblogController could be responsible for both posts and comments.
@ -33,7 +33,7 @@ A short rundown of the major features:
* Actions grouped in controller as methods instead of separate command objects * Actions grouped in controller as methods instead of separate command objects
and can therefore share helper methods and can therefore share helper methods
BlogController < ActionController::Base CustomersController < ActionController::Base
def show def show
@customer = find_customer @customer = find_customer
end end
@ -42,7 +42,7 @@ A short rundown of the major features:
@customer = find_customer @customer = find_customer
@customer.attributes = params[:customer] @customer.attributes = params[:customer]
@customer.save ? @customer.save ?
redirect_to(:action => "display") : redirect_to(:action => "show") :
render(:action => "edit") render(:action => "edit")
end end
@ -59,7 +59,7 @@ A short rundown of the major features:
Title: <%= post.title %> Title: <%= post.title %>
<% end %> <% end %>
All post titles: <%= @post.collect{ |p| p.title }.join ", " %> All post titles: <%= @posts.collect{ |p| p.title }.join ", " %>
<% unless @person.is_client? %> <% unless @person.is_client? %>
Not for clients to see... Not for clients to see...
@ -123,7 +123,7 @@ A short rundown of the major features:
<%= text_field "post", "title", "size" => 30 %> <%= text_field "post", "title", "size" => 30 %>
<%= html_date_select(Date.today) %> <%= html_date_select(Date.today) %>
<%= link_to "New post", :controller => "post", :action => "new" %> <%= link_to "New post", :controller => "post", :action => "new" %>
<%= truncate(post.title, 25) %> <%= truncate(post.title, :length => 25) %>
{Learn more}[link:classes/ActionView/Helpers.html] {Learn more}[link:classes/ActionView/Helpers.html]
@ -177,21 +177,6 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionView/Helpers/JavaScriptHelper.html] {Learn more}[link:classes/ActionView/Helpers/JavaScriptHelper.html]
* Pagination for navigating lists of results
# controller
def list
@pages, @people =
paginate :people, :order => 'last_name, first_name'
end
# view
<%= link_to "Previous page", { :page => @pages.current.previous } if @pages.current.previous %>
<%= link_to "Next page", { :page => @pages.current.next } if @pages.current.next %>
{Learn more}[link:classes/ActionController/Pagination.html]
* Easy testing of both controller and rendered template through ActionController::TestCase * Easy testing of both controller and rendered template through ActionController::TestCase
class LoginControllerTest < ActionController::TestCase class LoginControllerTest < ActionController::TestCase
@ -215,11 +200,11 @@ A short rundown of the major features:
If Active Record is used as the model, you'll have the database debugging If Active Record is used as the model, you'll have the database debugging
as well: as well:
Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23) Processing PostsController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
Params: {"controller"=>"weblog", "action"=>"create", Params: {"controller"=>"posts", "action"=>"create",
"post"=>{"title"=>"this is good"} } "post"=>{"title"=>"this is good"} }
SQL (0.000627) INSERT INTO posts (title) VALUES('this is good') SQL (0.000627) INSERT INTO posts (title) VALUES('this is good')
Redirected to http://test/weblog/display/5 Redirected to http://example.com/posts/5
Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%) Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%)
You specify a logger through a class method, such as: You specify a logger through a class method, such as:
@ -256,30 +241,6 @@ A short rundown of the major features:
{Learn more}[link:classes/ActionController/Caching.html] {Learn more}[link:classes/ActionController/Caching.html]
* Component requests from one controller to another
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"
end
end
class GreeterController < ActionController::Base
def hello_world
render_text "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" %>
{Learn more}[link:classes/ActionController/Components.html]
* Powerful debugging mechanism for local requests * Powerful debugging mechanism for local requests
All exceptions raised on actions performed on the request of a local user All exceptions raised on actions performed on the request of a local user
@ -336,7 +297,7 @@ A short rundown of the major features:
class WeblogController < ActionController::Base class WeblogController < ActionController::Base
def create def create
post = Post.create(params[:post]) post = Post.create(params[:post])
redirect_to :action => "display", :id => post.id redirect_to :action => "show", :id => post.id
end end
end end
@ -362,7 +323,7 @@ methods:
@posts = Post.find(:all) @posts = Post.find(:all)
end end
def display def show
@post = Post.find(params[:id]) @post = Post.find(params[:id])
end end
@ -372,7 +333,7 @@ methods:
def create def create
@post = Post.create(params[:post]) @post = Post.create(params[:post])
redirect_to :action => "display", :id => @post.id redirect_to :action => "show", :id => @post.id
end end
end end
@ -385,48 +346,33 @@ request from the web-server (like to be Apache).
And the templates look like this: And the templates look like this:
weblog/layout.erb: weblog/layout.html.erb:
<html><body> <html><body>
<%= yield %> <%= yield %>
</body></html> </body></html>
weblog/index.erb: weblog/index.html.erb:
<% for post in @posts %> <% for post in @posts %>
<p><%= link_to(post.title, :action => "display", :id => post.id %></p> <p><%= link_to(post.title, :action => "show", :id => post.id) %></p>
<% end %> <% end %>
weblog/display.erb: weblog/show.html.erb:
<p> <p>
<b><%= post.title %></b><br/> <b><%= @post.title %></b><br/>
<b><%= post.content %></b> <b><%= @post.content %></b>
</p> </p>
weblog/new.erb: weblog/new.html.erb:
<%= form "post" %> <%= form "post" %>
This simple setup will list all the posts in the system on the index page, This simple setup will list all the posts in the system on the index page,
which is called by accessing /weblog/. It uses the form builder for the Active which is called by accessing /weblog/. It uses the form builder for the Active
Record model to make the new screen, which in turn hands everything over to Record model to make the new screen, which in turn hands everything over to
the create action (that's the default target for the form builder when given a the create action (that's the default target for the form builder when given a
new model). After creating the post, it'll redirect to the display page using new model). After creating the post, it'll redirect to the show page using
an URL such as /weblog/display/5 (where 5 is the id of the post). an URL such as /weblog/5 (where 5 is the id of the post).
== Examples
Action Pack ships with three examples that all demonstrate an increasingly
detailed view of the possibilities. First is blog_controller that is just a
single file for the whole MVC (but still split into separate parts). Second is
the debate_controller that uses separate template files and multiple screens.
Third is the address_book_controller that uses the layout feature to separate
template casing from content.
Please note that you might need to change the "shebang" line to
#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby
Also note that these examples are all for demonstrating using Action Pack on
its own. Not for when it's used inside of Rails.
== Download == Download
The latest version of Action Pack can be found at The latest version of Action Pack can be found at

View file

@ -4,7 +4,6 @@ require 'rake/testtask'
require 'rake/rdoctask' require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -30,7 +29,7 @@ Rake::TestTask.new(:test_action_pack) do |t|
# make sure we include the tests in alphabetical order as on some systems # make sure we include the tests in alphabetical order as on some systems
# this will not happen automatically and the tests (as a whole) will error # this will not happen automatically and the tests (as a whole) will error
t.test_files = Dir.glob( "test/[cft]*/**/*_test.rb" ).sort t.test_files = Dir.glob( "test/[cftv]*/**/*_test.rb" ).sort
t.verbose = true t.verbose = true
#t.warning = true #t.warning = true
@ -80,7 +79,8 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 2.2.2' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.5' + PKG_BUILD)
s.add_dependency('rack', '~> 1.0.0')
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'action_controller' s.autorequire = 'action_controller'
@ -136,12 +136,14 @@ task :update_js => [ :update_scriptaculous ]
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
require 'rake/contrib/sshpublisher'
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'` `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
require 'rake/contrib/sshpublisher'
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload
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 # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the
@ -31,49 +31,83 @@ rescue LoadError
end end
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' module ActionController
require 'action_controller/request' # TODO: Review explicit to see if they will automatically be handled by
require 'action_controller/rescue' # the initilizer if they are really needed.
require 'action_controller/benchmarking' def self.load_all!
require 'action_controller/flash' [Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
require 'action_controller/filters' end
require 'action_controller/layout'
require 'action_controller/mime_responds' autoload :Base, 'action_controller/base'
require 'action_controller/helpers' autoload :Benchmarking, 'action_controller/benchmarking'
require 'action_controller/cookies' autoload :Caching, 'action_controller/caching'
require 'action_controller/cgi_process' autoload :Cookies, 'action_controller/cookies'
require 'action_controller/caching' autoload :Dispatcher, 'action_controller/dispatcher'
require 'action_controller/verification' autoload :Failsafe, 'action_controller/failsafe'
require 'action_controller/streaming' autoload :Filters, 'action_controller/filters'
require 'action_controller/session_management' autoload :Flash, 'action_controller/flash'
require 'action_controller/http_authentication' autoload :Helpers, 'action_controller/helpers'
require 'action_controller/components' autoload :HttpAuthentication, 'action_controller/http_authentication'
require 'action_controller/rack_process' autoload :Integration, 'action_controller/integration'
require 'action_controller/record_identifier' autoload :IntegrationTest, 'action_controller/integration'
require 'action_controller/request_forgery_protection' autoload :Layout, 'action_controller/layout'
require 'action_controller/headers' autoload :MiddlewareStack, 'action_controller/middleware_stack'
require 'action_controller/translation' 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' 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 ActionController
module Assertions module Assertions
module DomAssertions 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) # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
# #
# ==== Examples # ==== Examples
@ -12,13 +24,15 @@ module ActionController
clean_backtrace do clean_backtrace do
expected_dom = HTML::Document.new(expected).root expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s) 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 } assert_block(full_message) { expected_dom == actual_dom }
end end
end end
# The negated form of +assert_dom_equivalent+. # The negated form of +assert_dom_equal+.
# #
# ==== Examples # ==== Examples
# #
@ -29,8 +43,10 @@ module ActionController
clean_backtrace do clean_backtrace do
expected_dom = HTML::Document.new(expected).root expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root actual_dom = HTML::Document.new(actual).root
full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s) 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 } assert_block(full_message) { expected_dom != actual_dom }
end end
end end

View file

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

View file

@ -1,6 +1,3 @@
require 'rexml/document'
require 'html/document'
module ActionController module ActionController
module Assertions module Assertions
# A small suite of assertions that test responses from Rails applications. # A small suite of assertions that test responses from Rails applications.
@ -66,7 +63,12 @@ module ActionController
# Support partial arguments for hash redirections # Support partial arguments for hash redirections
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash) 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 end
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to) redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
@ -78,29 +80,64 @@ module ActionController
end end
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 # ==== Examples
# #
# # assert that the "new" view template was rendered # # assert that the "new" view template was rendered
# assert_template "new" # 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 clean_backtrace do
rendered = @response.rendered_template.to_s case options
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered) when NilClass, String, Symbol
rendered = @response.rendered[:template].to_s
msg = build_message(message,
"expecting <?> but rendering with <?>",
options, rendered)
assert_block(msg) do assert_block(msg) do
if expected.nil? if options.nil?
@response.rendered_template.blank? @response.rendered[:template].blank?
else else
rendered.to_s.match(expected) rendered.to_s.match(options.to_s)
end end
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
end end
private private
# Proxy to to_param if the object will respond to it. # Proxy to to_param if the object will respond to it.
def parameterize(value) def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value value.respond_to?(:to_param) ? value.to_param : value

View file

@ -134,7 +134,7 @@ module ActionController
path = "/#{path}" unless path.first == '/' path = "/#{path}" unless path.first == '/'
# Assume given controller # 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.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path request.path = path

View file

@ -3,9 +3,6 @@
# Under MIT and/or CC By license. # Under MIT and/or CC By license.
#++ #++
require 'rexml/document'
require 'html/document'
module ActionController module ActionController
module Assertions module Assertions
unless const_defined?(:NO_STRIP) unless const_defined?(:NO_STRIP)
@ -27,6 +24,12 @@ module ActionController
# #
# Also see HTML::Selector to learn how to use selectors. # Also see HTML::Selector to learn how to use selectors.
module SelectorAssertions module SelectorAssertions
def initialize(*args)
super
@selected = nil
end
# :call-seq: # :call-seq:
# css_select(selector) => array # css_select(selector) => array
# css_select(element, selector) => array # css_select(element, selector) => array
@ -112,20 +115,27 @@ module ActionController
# starting from (and including) that element and all its children in # starting from (and including) that element and all its children in
# depth-first order. # depth-first order.
# #
# If no element if specified, calling +assert_select+ will select from the # If no element if specified, calling +assert_select+ selects from the
# response HTML. Calling #assert_select inside an +assert_select+ block will # response HTML unless +assert_select+ is called from within an +assert_select+ block.
# run the assertion for each element selected by the enclosing assertion. #
# 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 # ==== 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| # elements.each do |element|
# assert_select element, "li" # assert_select element, "li", 4
# end # end
# end # end
# #
# Or for short: # will pass, as will:
# assert_select "ol>li" do # assert_select "ol" do
# assert_select "li" # assert_select "li", 8
# end # end
# #
# The selector may be a CSS selector expression (String), an expression # The selector may be a CSS selector expression (String), an expression
@ -405,6 +415,7 @@ module ActionController
if rjs_type if rjs_type
if rjs_type == :insert if rjs_type == :insert
position = args.shift position = args.shift
id = args.shift
insertion = "insert_#{position}".to_sym insertion = "insert_#{position}".to_sym
raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion] raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion]
statement = "(#{RJS_STATEMENTS[insertion]})" statement = "(#{RJS_STATEMENTS[insertion]})"
@ -590,7 +601,7 @@ module ActionController
def response_from_page_or_rjs() def response_from_page_or_rjs()
content_type = @response.content_type content_type = @response.content_type
if content_type && content_type =~ /text\/javascript/ if content_type && Mime::JS =~ content_type
body = @response.body.dup body = @response.body.dup
root = HTML::Node.new(nil) root = HTML::Node.new(nil)

View file

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

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' require 'set'
module ActionController #:nodoc: module ActionController #:nodoc:
@ -31,7 +22,7 @@ module ActionController #:nodoc:
attr_reader :allowed_methods attr_reader :allowed_methods
def initialize(*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 @allowed_methods = allowed_methods
end end
@ -173,8 +164,8 @@ module ActionController #:nodoc:
# #
# Other options for session storage are: # Other options for session storage are:
# #
# * ActiveRecordStore - Sessions are stored in your database, which works better than PStore with multiple app servers and, # * 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 ActiveRecordStore, set # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set
# #
# config.action_controller.session_store = :active_record_store # config.action_controller.session_store = :active_record_store
# #
@ -263,7 +254,7 @@ module ActionController #:nodoc:
cattr_reader :protected_instance_variables cattr_reader :protected_instance_variables
# Controller specific instance variables which will not be accessible inside views. # 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 @@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) @_flash @_response)
# Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets, # 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: # A YAML parser is also available and can be turned on with:
# #
# ActionController::Base.param_parsers[Mime::YAML] = :yaml # ActionController::Base.param_parsers[Mime::YAML] = :yaml
@@param_parsers = { Mime::MULTIPART_FORM => :multipart_form, @@param_parsers = {}
Mime::URL_ENCODED_FORM => :url_encoded_form,
Mime::XML => :xml_simple,
Mime::JSON => :json }
cattr_accessor :param_parsers cattr_accessor :param_parsers
# Controls the default charset for all renders. # Controls the default charset for all renders.
@ -336,6 +324,10 @@ module ActionController #:nodoc:
# sets it to <tt>:authenticity_token</tt> by default. # sets it to <tt>:authenticity_token</tt> by default.
cattr_accessor :request_forgery_protection_token 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 # Indicates whether or not optimise the generated named
# route helper methods # route helper methods
cattr_accessor :optimise_named_routes cattr_accessor :optimise_named_routes
@ -387,6 +379,13 @@ module ActionController #:nodoc:
attr_accessor :action_name attr_accessor :action_name
class << self 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. # Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc: def process(request, response) #:nodoc:
new.process(request, response) new.process(request, response)
@ -492,9 +491,18 @@ module ActionController #:nodoc:
filtered_parameters[key] = '[FILTERED]' filtered_parameters[key] = '[FILTERED]'
elsif value.is_a?(Hash) elsif value.is_a?(Hash)
filtered_parameters[key] = filter_parameters(value) 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? elsif block_given?
key = key.dup key = key.dup
value = value.dup if value value = value.dup if value.duplicable?
yield key, value yield key, value
filtered_parameters[key] = value filtered_parameters[key] = value
else else
@ -507,7 +515,7 @@ module ActionController #:nodoc:
protected :filter_parameters protected :filter_parameters
end end
delegate :exempt_from_layout, :to => 'ActionView::Base' delegate :exempt_from_layout, :to => 'ActionView::Template'
end end
public public
@ -529,7 +537,7 @@ module ActionController #:nodoc:
end end
def send_response def send_response
response.prepare! unless component_request? response.prepare!
response response
end end
@ -645,7 +653,7 @@ module ActionController #:nodoc:
end end
def session_enabled? 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 end
self.view_paths = [] self.view_paths = []
@ -785,9 +793,36 @@ module ActionController #:nodoc:
# # placed in "app/views/layouts/special.r(html|xml)" # # placed in "app/views/layouts/special.r(html|xml)"
# render :text => "Hi there!", :layout => "special" # 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 # === Streaming data and/or controlling the page generation
# 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. # 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!" # # Renders "Hello from code!"
# render :text => proc { |response, output| output.write("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: def render(options = nil, extra_options = {}, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed? raise DoubleRenderError, "Can only render or redirect once per action" if performed?
validate_render_arguments(options, extra_options, block_given?)
if options.nil? if options.nil?
return render(:file => default_template_name, :layout => true) options = { :template => default_template, :layout => true }
elsif !extra_options.is_a?(Hash) elsif options == :update
raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
else
if options == :update
options = extra_options.merge({ :update => true }) options = extra_options.merge({ :update => true })
elsif !options.is_a?(Hash) elsif options.is_a?(String) || options.is_a?(Symbol)
raise RenderError, "You called render with invalid options : #{options.inspect}" case options.to_s.index('/')
end when 0
extra_options[:file] = options
when nil
extra_options[:action] = options
else
extra_options[:template] = options
end end
response.layout = layout = pick_layout(options) options = extra_options
logger.info("Rendering template within #{layout}") if logger && layout elsif !options.is_a?(Hash)
extra_options[:partial] = options
options = extra_options
end
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] if content_type = options[:content_type]
response.content_type = content_type.to_s 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]) render_for_text(@template.render(options.merge(:layout => layout)), options[:status])
elsif action_name = options[:action] 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] elsif xml = options[:xml]
response.content_type ||= Mime::XML response.content_type ||= Mime::XML
@ -912,8 +958,9 @@ module ActionController #:nodoc:
response.content_type ||= Mime::JS response.content_type ||= Mime::JS
render_for_text(js, options[:status]) render_for_text(js, options[:status])
elsif json = options[:json] elsif options.include?(:json)
json = json.to_json unless json.is_a?(String) json = options[:json]
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank? json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
response.content_type ||= Mime::JSON response.content_type ||= Mime::JSON
render_for_text(json, options[:status]) render_for_text(json, options[:status])
@ -937,7 +984,7 @@ module ActionController #:nodoc:
render_for_text(nil, options[:status]) render_for_text(nil, options[:status])
else else
render_for_file(default_template_name, options[:status], layout) render_for_file(default_template, options[:status], layout)
end end
end end
end end
@ -994,7 +1041,7 @@ module ActionController #:nodoc:
@performed_redirect = false @performed_redirect = false
response.redirected_to = nil response.redirected_to = nil
response.redirected_to_method_params = nil response.redirected_to_method_params = nil
response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE response.status = DEFAULT_RENDER_STATUS_CODE
response.headers.delete('Location') response.headers.delete('Location')
end end
@ -1065,7 +1112,6 @@ module ActionController #:nodoc:
end end
response.redirected_to = options response.redirected_to = options
logger.info("Redirected to #{options}") if logger && logger.info?
case options case options
# The scheme name consist of a letter followed by any combination of # 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) def redirect_to_full_url(url, status)
raise DoubleRenderError if performed? raise DoubleRenderError if performed?
logger.info("Redirected to #{url}") if logger && logger.info?
response.redirect(url, interpret_status(status)) response.redirect(url, interpret_status(status))
@performed_redirect = true @performed_redirect = true
end end
@ -1097,6 +1144,11 @@ module ActionController #:nodoc:
# request is considered stale and should be generated from scratch. Otherwise, # 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. # 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: # Example:
# #
# def show # def show
@ -1117,21 +1169,35 @@ module ActionController #:nodoc:
# Sets the etag, last_modified, or both on the response and renders a # 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: # Example:
# #
# def show # def show
# @article = Article.find(params[:id]) # @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 # 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. # If-Modified-Since header and just a "304 Not Modified" response if there's a match.
#
def fresh_when(options) 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.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified] 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) if request.fresh?(response)
head :not_modified head :not_modified
end end
@ -1142,15 +1208,26 @@ module ActionController #:nodoc:
# #
# Examples: # Examples:
# expires_in 20.minutes # expires_in 20.minutes
# expires_in 3.hours, :private => false # expires_in 3.hours, :public => true
# expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true # expires in 3.hours, 'max-stale' => 5.hours, :public => true
# #
# This method will overwrite an existing Cache-Control header. # This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc: def expires_in(seconds, options = {}) #:doc:
cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys) cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
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 << "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(', ') response.headers["Cache-Control"] = cache_control.join(', ')
end end
@ -1164,20 +1241,19 @@ module ActionController #:nodoc:
def reset_session #:doc: def reset_session #:doc:
request.reset_session request.reset_session
@_session = request.session @_session = request.session
response.session = @_session
end end
private private
def render_for_file(template_path, status = nil, layout = nil, locals = {}) #:nodoc: 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 render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status
end end
def render_for_text(text = nil, status = nil, append_response = false) #:nodoc: def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true @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 if append_response
response.body ||= '' response.body ||= ''
@ -1191,6 +1267,16 @@ module ActionController #:nodoc:
end end
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) def initialize_template_class(response)
response.template = ActionView::Base.new(self.class.view_paths, {}, self) response.template = ActionView::Base.new(self.class.view_paths, {}, self)
response.template.helpers.send :include, self.class.master_helper_module response.template.helpers.send :include, self.class.master_helper_module
@ -1199,7 +1285,7 @@ module ActionController #:nodoc:
end end
def assign_shortcuts(request, response) def assign_shortcuts(request, response)
@_request, @_params, @_cookies = request, request.parameters, request.cookies @_request, @_params = request, request.parameters
@_response = response @_response = response
@_response.session = request.session @_response.session = request.session
@ -1217,7 +1303,6 @@ module ActionController #:nodoc:
def log_processing def log_processing
if logger && logger.info? if logger && logger.info?
log_processing_for_request_id log_processing_for_request_id
log_processing_for_session_id
log_processing_for_parameters log_processing_for_parameters
end end
end end
@ -1230,13 +1315,6 @@ module ActionController #:nodoc:
logger.info(request_id) logger.info(request_id)
end 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 def log_processing_for_parameters
parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
parameters = parameters.except!(:controller, :action, :format, :_method) parameters = parameters.except!(:controller, :action, :format, :_method)
@ -1255,10 +1333,17 @@ module ActionController #:nodoc:
elsif respond_to? :method_missing elsif respond_to? :method_missing
method_missing action_name method_missing action_name
default_render unless performed? default_render unless performed?
elsif template_exists?
default_render
else 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
end end
@ -1270,11 +1355,6 @@ module ActionController #:nodoc:
@action_name = (params['action'] || 'index') @action_name = (params['action'] || 'index')
end 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 def action_methods
self.class.action_methods self.class.action_methods
end end
@ -1305,14 +1385,8 @@ module ActionController #:nodoc:
"#{request.protocol}#{request.host}#{request.request_uri}" "#{request.protocol}#{request.host}#{request.request_uri}"
end end
def close_session def default_template(action_name = self.action_name)
@_session.close if @_session && @_session.respond_to?(:close) self.view_paths.find_template(default_template_name(action_name), default_template_format)
end
def template_exists?(template_name = default_template_name)
@template.send(:_pick_template, template_name) ? true : false
rescue ActionView::MissingTemplate
false
end end
def default_template_name(action_name = self.action_name) def default_template_name(action_name = self.action_name)
@ -1334,7 +1408,16 @@ module ActionController #:nodoc:
end end
def process_cleanup 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 end
end end

View file

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

View file

@ -2,13 +2,6 @@ require 'fileutils'
require 'uri' require 'uri'
require 'set' 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: module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls # 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. # 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 = :file_store, "/path/to/cache/directory"
# ActionController::Base.cache_store = :drb_store, "druby://localhost:9192" # ActionController::Base.cache_store = :drb_store, "druby://localhost:9192"
# ActionController::Base.cache_store = :mem_cache_store, "localhost" # 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") # ActionController::Base.cache_store = MyOwnStore.new("parameter")
module Caching 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: def self.included(base) #:nodoc:
base.class_eval do base.class_eval do
@@cache_store = nil @@cache_store = nil
@ -42,7 +42,7 @@ module ActionController #:nodoc:
end end
include Pages, Actions, Fragments include Pages, Actions, Fragments
include Sweeping, SqlCache if defined?(ActiveRecord) include Sweeping if defined?(ActiveRecord)
@@perform_caching = true @@perform_caching = true
cattr_accessor :perform_caching cattr_accessor :perform_caching
@ -63,7 +63,6 @@ module ActionController #:nodoc:
end end
end end
private private
def cache_configured? def cache_configured?
self.class.cache_configured? self.class.cache_configured?

View file

@ -61,7 +61,9 @@ module ActionController #:nodoc:
filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) } 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) 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
end end
@ -83,6 +85,12 @@ module ActionController #:nodoc:
@options = options @options = options
end end
def filter(controller, action)
should_continue = before(controller)
action.call if should_continue
after(controller)
end
def before(controller) def before(controller)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path))) cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
if cache = controller.read_fragment(cache_path.path, @options[:store_options]) if cache = controller.read_fragment(cache_path.path, @options[:store_options])
@ -113,7 +121,7 @@ module ActionController #:nodoc:
end end
def caching_allowed(controller) def caching_allowed(controller)
controller.request.get? && controller.response.headers['Status'].to_i == 200 controller.request.get? && controller.response.status.to_i == 200
end end
def cache_layout? def cache_layout?
@ -135,18 +143,17 @@ module ActionController #:nodoc:
end end
# When true, infer_extension will look up the cache path extension from the request's path & format. # 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. # 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) def initialize(controller, options = {}, infer_extension = true)
if infer_extension and options.is_a? Hash if infer_extension
request_extension = extract_extension(controller.request) extract_extension(controller.request)
options = options.reverse_merge(:format => request_extension) options = options.reverse_merge(:format => @extension) if options.is_a?(Hash)
end end
path = controller.url_for(options).split('://').last path = controller.url_for(options).split('://').last
normalize!(path) normalize!(path)
if infer_extension
@extension = request_extension
add_extension!(path, @extension) add_extension!(path, @extension)
end
@path = URI.unescape(path) @path = URI.unescape(path)
end end
@ -162,13 +169,7 @@ module ActionController #:nodoc:
def extract_extension(request) def extract_extension(request)
# Don't want just what comes after the last '.' to accommodate multi part extensions # Don't want just what comes after the last '.' to accommodate multi part extensions
# such as tar.gz. # such as tar.gz.
extension = request.path[/^[^.]+\.(.+)$/, 1] @extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format
# If there's no extension in the path, check request.format
if extension.nil?
extension = request.cache_format
end
extension
end end
end end
end end

View file

@ -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) # 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) def write_fragment(key, content, options = nil)
return unless cache_configured? return content unless cache_configured?
key = fragment_cache_key(key) key = fragment_cache_key(key)
@ -83,15 +83,23 @@ module ActionController #:nodoc:
end end
end end
# Name can take one of three forms: # Removes fragments from the cache.
# * 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 } # +key+ can take one of three forms:
# * Regexp: Will destroy all the matched fragments, example: # * String - This would normally take the form of a path, like
# %r{pages/\d*/notes} # <tt>"pages/45/notes"</tt>.
# Ensure you do not specify start and finish in the regex (^$) because # * Hash - Treated as an implicit call to +url_for+, like
# the actual filename matched looks like ./cache/filename/path.cache # <tt>{:controller => "pages", :action => "notes", :id => 45}</tt>
# Regexp expiration is only supported on caches that can iterate over # * Regexp - Will remove any fragment that matches, so
# all keys (unlike memcached). # <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) def expire_fragment(key, options = nil)
return unless cache_configured? 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 # Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
# expired. # 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 module Pages
def self.included(base) #:nodoc: def self.included(base) #:nodoc:
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval do base.class_eval do
@@page_cache_directory = defined?(Rails.public_path) ? Rails.public_path : "" @@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 cattr_accessor :page_cache_directory
@@page_cache_extension = '.html' @@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 cattr_accessor :page_cache_extension
end end
end end
@ -147,7 +145,7 @@ module ActionController #:nodoc:
private private
def caching_allowed def caching_allowed
request.get? && response.headers['Status'].to_i == 200 request.get? && response.status.to_i == 200
end end
end 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 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
end end

View file

@ -1,7 +1,6 @@
require 'action_controller/cgi_ext/stdinput' require 'action_controller/cgi_ext/stdinput'
require 'action_controller/cgi_ext/query_extension' require 'action_controller/cgi_ext/query_extension'
require 'action_controller/cgi_ext/cookie' require 'action_controller/cgi_ext/cookie'
require 'action_controller/cgi_ext/session'
class CGI #:nodoc: class CGI #:nodoc:
include ActionController::CgiExt::Stdinput include ActionController::CgiExt::Stdinput

View file

@ -1,3 +1,5 @@
require 'delegate'
CGI.module_eval { remove_const "Cookie" } CGI.module_eval { remove_const "Cookie" }
# TODO: document how this differs from stdlib CGI::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/cgi_ext'
require 'action_controller/session/cookie_store'
module ActionController #:nodoc: module ActionController #:nodoc:
class Base class CGIHandler
# Process a request extracted from a CGI object and return a response. Pass false as <tt>session_options</tt> to disable module ProperStream
# sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session: def each
# while line = gets
# * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore yield line
# (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
end end
class CgiRequest < AbstractRequest #:nodoc: def read(*args)
attr_accessor :cgi, :session_options if args.empty?
class SessionFixationAttempt < StandardError #:nodoc: super || ""
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 else
super super
end end
end end
def body_stream #:nodoc:
@cgi.stdinput
end end
def cookies def self.dispatch_cgi(app, cgi, out = $stdout)
@cgi.cookies.freeze env = cgi.__send__(:env_table)
end env.delete "HTTP_CONTENT_LENGTH"
def session cgi.stdinput.extend ProperStream
unless defined?(@session)
if @session_options == false env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
@session = Hash.new
else env.update({
stale_session_check! do "rack.version" => [0,1],
if cookie_only? && query_parameters[session_options_with_string_keys['session_key']] "rack.input" => cgi.stdinput,
raise SessionFixationAttempt "rack.errors" => $stderr,
end "rack.multithread" => false,
case value = session_options_with_string_keys['new_session'] "rack.multiprocess" => true,
when true "rack.run_once" => false,
@session = new_session "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
when false })
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 begin
@session = CGI::Session.new(@cgi, session_options_with_string_keys) out.binmode if out.respond_to?(:binmode)
# CGI::Session raises ArgumentError if 'new_session' == false out.sync = false if out.respond_to?(:sync=)
# and no session cookie or query param is present.
rescue ArgumentError headers['Status'] = status.to_s
@session = Hash.new
end if headers.include?('Set-Cookie')
when nil headers['cookie'] = headers.delete('Set-Cookie').split("\n")
@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 end
def reset_session out.write(cgi.header(headers))
@session.delete if defined?(@session) && @session.is_a?(CGI::Session)
@session = new_session
end
def method_missing(method_id, *arguments) body.each { |part|
@cgi.__send__(method_id, *arguments) rescue super out.write part
out.flush if out.respond_to?(:flush)
}
ensure
body.close if body.respond_to?(:close)
end 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
end end
def cookie_only? class CgiRequest #:nodoc:
session_options_with_string_keys['cookie_only'] DEFAULT_SESSION_OPTIONS = {
end :database_manager => nil,
:prefix => "ruby_sess.",
def stale_session_check! :session_path => "/",
yield :session_key => "_session_id",
rescue ArgumentError => argument_error :cookie_only => true,
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} :session_http_only => true
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 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=)
begin
output.write(@cgi.header(@headers))
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)
end
output.flush if output.respond_to?(:flush)
rescue Errno::EPIPE, Errno::ECONNRESET
# lost connection to parent process, ignore output
end
end
end end
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>: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. # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
# Default is +false+. # 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+. # only HTTP. Defaults to +false+.
module Cookies module Cookies
def self.included(base) def self.included(base)
@ -51,7 +51,7 @@ module ActionController #:nodoc:
protected protected
# Returns the cookie container, which operates as described above. # Returns the cookie container, which operates as described above.
def cookies def cookies
CookieJar.new(self) @cookies ||= CookieJar.new(self)
end end
end end
@ -64,43 +64,32 @@ module ActionController #:nodoc:
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
def [](name) def [](name)
cookie = @cookies[name.to_s] super(name.to_s)
if cookie && cookie.respond_to?(:value)
cookie.size > 1 ? cookie.value : cookie.value[0]
end
end end
# Sets the cookie named +name+. The second argument may be the very cookie # Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above. # value, or a hash of options as documented above.
def []=(name, options) def []=(key, options)
if options.is_a?(Hash) if options.is_a?(Hash)
options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options } options.symbolize_keys!
options["name"] = name.to_s
else else
options = { "name" => name.to_s, "value" => options } options = { :value => options }
end end
set_cookie(options) options[:path] = "/" unless options.has_key?(:path)
super(key.to_s, options[:value])
@controller.response.set_cookie(key, options)
end end
# Removes the cookie on the client machine by setting the value to an empty string # 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 # 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>. # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
def delete(name, options = {}) def delete(key, options = {})
options.stringify_keys! options.symbolize_keys!
set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0))) options[:path] = "/" unless options.has_key?(:path)
end value = super(key.to_s)
@controller.response.delete_cookie(key, options)
private value
# 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 end
end end

View file

@ -2,27 +2,16 @@ module ActionController
# Dispatches requests to the appropriate controller and takes care of # Dispatches requests to the appropriate controller and takes care of
# reloading the app after each request when Dependencies.load? is true. # reloading the app after each request when Dependencies.load? is true.
class Dispatcher class Dispatcher
@@guard = Mutex.new @@cache_classes = true
class << self class << self
def define_dispatcher_callbacks(cache_classes) def define_dispatcher_callbacks(cache_classes)
@@cache_classes = cache_classes
unless cache_classes unless cache_classes
# Development mode callbacks ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
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
end end
if defined?(ActiveRecord) if defined?(ActiveRecord)
after_dispatch :checkin_connections
to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers } to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
end end
@ -33,8 +22,7 @@ module ActionController
end end
end end
# Backward-compatible class method takes CGI-specific args. Deprecated # DEPRECATE: Remove CGI support
# in favor of Dispatcher.new(output, request, response).dispatch.
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout) def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
new(output).dispatch_cgi(cgi, session_options) new(output).dispatch_cgi(cgi, session_options)
end end
@ -53,144 +41,93 @@ module ActionController
@prepare_dispatch_callbacks.replace_or_append!(callback) @prepare_dispatch_callbacks.replace_or_append!(callback)
end end
# If the block raises, send status code as a last-ditch response. def run_prepare_callbacks
def failsafe_response(fallback_output, status, originating_exception = nil) if defined?(Rails) && Rails.logger
yield logger = Rails.logger
rescue Exception => exception
begin
log_failsafe_exception(status, originating_exception || exception)
body = failsafe_response_body(status)
fallback_output.write "Status: #{status}\r\nContent-Type: text/html\r\n\r\n#{body}"
nil
rescue Exception => failsafe_error # Logger or IO errors
$stderr.puts "Error during failsafe response: #{failsafe_error}"
$stderr.puts "(originally #{originating_exception})" if originating_exception
end
end
private
def failsafe_response_body(status)
error_path = "#{error_file_path}/#{status.to_s[0..3]}.html"
if File.exist?(error_path)
File.read(error_path)
else else
"<html><body><h1>#{status}</h1></body></html>" logger = Logger.new($stderr)
end
new(logger).send :run_callbacks, :prepare_dispatch
end
def reload_application
# Run prepare callbacks before every request in development mode
run_prepare_callbacks
Routing::Routes.reload
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
end end
def log_failsafe_exception(status, exception) cattr_accessor :middleware
message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: #{status}\n" self.middleware = MiddlewareStack.new do |middleware|
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception middlewares = File.join(File.dirname(__FILE__), "middlewares.rb")
failsafe_logger.fatal message middleware.instance_eval(File.read(middlewares))
end end
def failsafe_logger
if defined?(::RAILS_DEFAULT_LOGGER) && !::RAILS_DEFAULT_LOGGER.nil?
::RAILS_DEFAULT_LOGGER
else
Logger.new($stderr)
end
end
end
cattr_accessor :error_file_path
self.error_file_path = Rails.public_path if defined?(Rails.public_path)
include ActiveSupport::Callbacks include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch 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) def initialize(output = $stdout, request = nil, response = nil)
@output, @request, @response = output, request, response @output = output
build_middleware_stack if @@cache_classes
end end
def dispatch_unlocked def dispatch
begin begin
run_callbacks :before_dispatch run_callbacks :before_dispatch
handle_request Routing::Routes.call(@env)
rescue Exception => exception rescue Exception => exception
failsafe_rescue exception if controller ||= (::ApplicationController rescue Base)
controller.call_with_exception(@env, exception).to_a
else
raise exception
end
ensure ensure
run_callbacks :after_dispatch, :enumerator => :reverse_each run_callbacks :after_dispatch, :enumerator => :reverse_each
end end
end end
def dispatch # DEPRECATE: Remove CGI support
if ActionController::Base.allow_concurrency
dispatch_unlocked
else
@@guard.synchronize do
dispatch_unlocked
end
end
end
def dispatch_cgi(cgi, session_options) def dispatch_cgi(cgi, session_options)
if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new } CGIHandler.dispatch_cgi(self, cgi, @output)
@request = CgiRequest.new(cgi, session_options)
@response = CgiResponse.new(cgi)
dispatch
end
rescue Exception => exception
failsafe_rescue exception
end end
def call(env) def call(env)
@request = RackRequest.new(env) if @@cache_classes
@response = RackResponse.new(@request) @app.call(env)
else
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 _call(env)
@env = env
dispatch dispatch
end 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 def flush_logger
Base.logger.flush Base.logger.flush
end end
def mark_as_test_request! private
@test_request = true def build_middleware_stack
self @app = @@middleware.build(lambda { |env| self.dup._call(env) })
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
end end
end 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 # 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: # then expose the flash to its template. Actually, that exposure is automatically done. Example:
# #
# class WeblogController < ActionController::Base # class PostsController < ActionController::Base
# def create # def create
# # save post # # save post
# flash[:notice] = "Successfully created post" # flash[:notice] = "Successfully created post"
# redirect_to :action => "display", :params => { :id => post.id } # redirect_to posts_path(@post)
# end # end
# #
# def display # def show
# # doesn't need to assign the flash notice to the template, that's done automatically # # doesn't need to assign the flash notice to the template, that's done automatically
# end # end
# end # end
# #
# display.erb # show.html.erb
# <% if flash[:notice] %><div class="notice"><%= flash[:notice] %></div><% end %> # <% 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 # 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. # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
@ -27,12 +29,11 @@ module ActionController #:nodoc:
def self.included(base) def self.included(base)
base.class_eval do base.class_eval do
include InstanceMethods include InstanceMethods
alias_method_chain :assign_shortcuts, :flash alias_method_chain :perform_action, :flash
alias_method_chain :reset_session, :flash alias_method_chain :reset_session, :flash
end end
end end
class FlashNow #:nodoc: class FlashNow #:nodoc:
def initialize(flash) def initialize(flash)
@flash = flash @flash = flash
@ -119,6 +120,11 @@ module ActionController #:nodoc:
(@used.keys - keys).each{ |k| @used.delete(k) } (@used.keys - keys).each{ |k| @used.delete(k) }
end end
def store(session, key = "flash")
return if self.empty?
session[key] = self
end
private private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used # use() # marks the entire flash as used
@ -136,37 +142,30 @@ module ActionController #:nodoc:
module InstanceMethods #:nodoc: module InstanceMethods #:nodoc:
protected protected
def reset_session_with_flash def perform_action_with_flash
reset_session_without_flash perform_action_without_flash
if defined? @_flash
@_flash.store(session)
remove_instance_variable(:@_flash) remove_instance_variable(:@_flash)
flash(:refresh) end
end end
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or def reset_session_with_flash
# <tt>flash["notice"] = "hello"</tt> to put a new one. reset_session_without_flash
# Note that if sessions are disabled only flash.now will work. remove_instance_variable(:@_flash) if defined? @_flash
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 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 end
@_flash @_flash
end 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 end
end end

View file

@ -1,13 +1,17 @@
require 'active_support/dependencies'
# FIXME: helper { ... } is broken on Ruby 1.9 # FIXME: helper { ... } is broken on Ruby 1.9
module ActionController #:nodoc: module ActionController #:nodoc:
module Helpers #:nodoc: module Helpers #:nodoc:
HELPERS_DIR = (defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers")
def self.included(base) def self.included(base)
# Initialize the base module to aggregate its helpers. # Initialize the base module to aggregate its helpers.
base.class_inheritable_accessor :master_helper_module base.class_inheritable_accessor :master_helper_module
base.master_helper_module = Module.new 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. # Extend base with class methods to declare helpers.
base.extend(ClassMethods) 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. # When the argument is a module it will be included directly in the template class.
# helper FooHelper # => includes FooHelper # helper FooHelper # => includes FooHelper
# #
# When the argument is the symbol <tt>:all</tt>, the controller will include all helpers from # When the argument is the symbol <tt>:all</tt>, the controller will include all helpers beneath
# <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT. # <tt>ActionController::Base.helpers_dir</tt> (defaults to <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT).
# helper :all # helper :all
# #
# Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available # 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) def helper_method(*methods)
methods.flatten.each do |method| methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval master_helper_module.module_eval <<-end_eval
def #{method}(*args, &block) def #{method}(*args, &block) # def current_user(*args, &block)
controller.send(%(#{method}), *args, &block) controller.send(%(#{method}), *args, &block) # controller.send(%(current_user), *args, &block)
end end # end
end_eval end_eval
end end
end end
@ -213,8 +217,8 @@ module ActionController #:nodoc:
# Extract helper names from files in app/helpers/**/*.rb # Extract helper names from files in app/helpers/**/*.rb
def all_application_helpers def all_application_helpers
extract = /^#{Regexp.quote(HELPERS_DIR)}\/?(.*)_helper.rb$/ extract = /^#{Regexp.quote(helpers_dir)}\/?(.*)_helper.rb$/
Dir["#{HELPERS_DIR}/**/*_helper.rb"].map { |file| file.sub extract, '\1' } Dir["#{helpers_dir}/**/*_helper.rb"].map { |file| file.sub extract, '\1' }
end end
end end
end end

View file

@ -55,7 +55,6 @@ module ActionController
# end # end
# end # end
# #
#
# In your integration tests, you can do something like this: # In your integration tests, you can do something like this:
# #
# def test_access_granted_from_xml # def test_access_granted_from_xml
@ -67,6 +66,38 @@ module ActionController
# assert_equal 200, status # assert_equal 200, status
# end # 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 # On shared hosts, Apache sometimes doesn't pass authentication headers to
# FCGI instances. If your environment matches this description and you cannot # FCGI instances. If your environment matches this description and you cannot
@ -108,7 +139,7 @@ module ActionController
end end
def decode_credentials(request) def decode_credentials(request)
ActiveSupport::Base64.decode64(authorization(request).split.last || '') ActiveSupport::Base64.decode64(authorization(request).split(' ', 2).last || '')
end end
def encode_credentials(user_name, password) def encode_credentials(user_name, password)
@ -120,5 +151,159 @@ module ActionController
controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end end
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
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 'stringio'
require 'uri' require 'uri'
require 'active_support/test_case'
require 'action_controller/rack_lint_patch'
module ActionController module ActionController
module Integration #:nodoc: module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses # 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 # multiple sessions and run them side-by-side, you can also mimic (to some
# limited extent) multiple simultaneous users interacting with your system. # limited extent) multiple simultaneous users interacting with your system.
# #
# Typically, you will instantiate a new session using IntegrationTest#open_session, # Typically, you will instantiate a new session using
# rather than instantiating Integration::Session directly. # IntegrationTest#open_session, rather than instantiating
# Integration::Session directly.
class Session class Session
include Test::Unit::Assertions include Test::Unit::Assertions
include ActionController::Assertions include ActionController::TestCase::Assertions
include ActionController::TestProcess include ActionController::TestProcess
# Rack application to use
attr_accessor :application
# The integer HTTP status code of the last request. # The integer HTTP status code of the last request.
attr_reader :status attr_reader :status
# The status message that accompanied the status code of the last request. # The status message that accompanied the status code of the last request.
attr_reader :status_message attr_reader :status_message
# The body of the last request.
attr_reader :body
# The URI of the last request. # The URI of the last request.
attr_reader :path attr_reader :path
@ -60,7 +65,8 @@ module ActionController
end end
# Create and initialize a new Session instance. # Create and initialize a new Session instance.
def initialize def initialize(app = nil)
@application = app || ActionController::Dispatcher.new
reset! reset!
end end
@ -79,7 +85,9 @@ module ActionController
self.host = "www.example.com" self.host = "www.example.com"
self.remote_addr = "127.0.0.1" 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 unless defined? @named_routes_configured
# install the named routes in this session instance. # install the named routes in this session instance.
@ -122,7 +130,7 @@ module ActionController
# performed on the location header. # performed on the location header.
def follow_redirect! def follow_redirect!
raise "not a redirect! #{@status} #{@status_message}" unless redirect? raise "not a redirect! #{@status} #{@status_message}" unless redirect?
get(interpret_uri(headers['location'].first)) get(interpret_uri(headers['location']))
status status
end end
@ -167,17 +175,21 @@ module ActionController
# Performs a GET request with the given parameters. # Performs a GET request with the given parameters.
# #
# - +path+: The URI (as a String) on which you want to perform a GET request. # - +path+: The URI (as a String) on which you want to perform a GET
# - +parameters+: The HTTP parameters that you want to pass. This may be +nil+, # request.
# - +parameters+: The HTTP parameters that you want to pass. This may
# be +nil+,
# a Hash, or a String that is appropriately encoded # 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 # - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
# automatically be upcased, with the prefix 'HTTP_' added if needed. # automatically be upcased, with the prefix 'HTTP_' added if needed.
# #
# This method returns an AbstractResponse object, which one can use to inspect # This method returns an Response object, which one can use to
# the details of the response. Furthermore, if this method was called from an # inspect the details of the response. Furthermore, if this method was
# ActionController::IntegrationTest object, then that object's <tt>@response</tt> # called from an ActionController::IntegrationTest object, then that
# instance variable will point to the same response object. # 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+, # You can also perform POST, PUT, DELETE, and HEAD requests with +post+,
# +put+, +delete+, and +head+. # +put+, +delete+, and +head+.
@ -185,22 +197,26 @@ module ActionController
process :get, path, parameters, headers process :get, path, parameters, headers
end 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) def post(path, parameters = nil, headers = nil)
process :post, path, parameters, headers process :post, path, parameters, headers
end 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) def put(path, parameters = nil, headers = nil)
process :put, path, parameters, headers process :put, path, parameters, headers
end 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) def delete(path, parameters = nil, headers = nil)
process :delete, path, parameters, headers process :delete, path, parameters, headers
end 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) def head(path, parameters = nil, headers = nil)
process :head, path, parameters, headers process :head, path, parameters, headers
end end
@ -215,8 +231,7 @@ module ActionController
def xml_http_request(request_method, path, parameters = nil, headers = nil) def xml_http_request(request_method, path, parameters = nil, headers = nil)
headers ||= {} headers ||= {}
headers['X-Requested-With'] = 'XMLHttpRequest' 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) process(request_method, path, parameters, headers)
end end
alias xhr :xml_http_request alias xhr :xml_http_request
@ -224,7 +239,9 @@ module ActionController
# Returns the URL for the given options, according to the rules specified # Returns the URL for the given options, according to the rules specified
# in the application's routes. # in the application's routes.
def url_for(options) 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 end
private private
@ -250,17 +267,35 @@ module ActionController
data = nil data = nil
end 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( 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, "REQUEST_URI" => path,
"PATH_INFO" => path,
"HTTP_HOST" => host, "HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr, "REMOTE_ADDR" => remote_addr,
"SERVER_PORT" => (https? ? "443" : "80"),
"CONTENT_TYPE" => "application/x-www-form-urlencoded", "CONTENT_TYPE" => "application/x-www-form-urlencoded",
"CONTENT_LENGTH" => data ? data.length.to_s : nil, "CONTENT_LENGTH" => data ? data.length.to_s : nil,
"HTTP_COOKIE" => encode_cookies, "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| (headers || {}).each do |key, value|
@ -269,54 +304,62 @@ module ActionController
env[key] = value env[key] = value
end end
unless ActionController::Base.respond_to?(:clear_last_instantiation!) [ControllerCapture, ActionController::ProcessWithTest].each do |mod|
ActionController::Base.module_eval { include ControllerCapture } unless ActionController::Base < mod
ActionController::Base.class_eval { include mod }
end
end end
ActionController::Base.clear_last_instantiation! ActionController::Base.clear_last_instantiation!
env['rack.input'] = data.is_a?(IO) ? data : StringIO.new(data || '') app = Rack::Lint.new(@application)
@status, @headers, result_body = ActionController::Dispatcher.new.mark_as_test_request!.call(env) status, headers, body = app.call(env)
@request_count += 1 @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 @html_document = nil
# Inject status back in for backwords compatibility with CGI @status = status.to_i
@headers['Status'] = @status @status_message = StatusCodes::STATUS_CODES[@status]
@status, @status_message = @status.split(/ /) @headers = Rack::Utils::HeaderHash.new(headers)
@status = @status.to_i
cgi_headers = Hash.new { |h,k| h[k] = [] } (@headers['Set-Cookie'] || "").split("\n").each do |cookie|
@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|
name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2] name, value = cookie.match(/^([^=]*)=([^;]*);/)[1,2]
@cookies[name] = value @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 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 rescue MultiPartNeededException
boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1" 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 return status
end end
@ -338,7 +381,7 @@ module ActionController
"SERVER_PORT" => https? ? "443" : "80", "SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off" "HTTPS" => https? ? "on" : "off"
} }
ActionController::UrlRewriter.new(ActionController::RackRequest.new(env), {}) UrlRewriter.new(Request.new(env), {})
end end
def name_with_prefix(prefix, name) def name_with_prefix(prefix, name)
@ -352,9 +395,13 @@ module ActionController
raise MultiPartNeededException raise MultiPartNeededException
elsif Hash === parameters elsif Hash === parameters
return nil if parameters.empty? 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 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? elsif prefix.nil?
parameters parameters
else else
@ -365,7 +412,7 @@ module ActionController
def multipart_requestify(params, first=true) def multipart_requestify(params, first=true)
returning Hash.new do |p| returning Hash.new do |p|
params.each do |key, value| 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 if Hash === value
multipart_requestify(value, false).each do |subkey, subvalue| multipart_requestify(value, false).each do |subkey, subvalue|
p[k + subkey] = subvalue p[k + subkey] = subvalue
@ -380,7 +427,7 @@ module ActionController
def multipart_body(params, boundary) def multipart_body(params, boundary)
multipart_requestify(params).map do |key, value| multipart_requestify(params).map do |key, value|
if value.respond_to?(:original_filename) 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) f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
<<-EOF <<-EOF
@ -432,6 +479,11 @@ EOF
end end
module Runner module Runner
def initialize(*args)
super
@integration_session = nil
end
# Reset the current session. This is useful for testing multiple sessions # Reset the current session. This is useful for testing multiple sessions
# in a single test case. # in a single test case.
def reset! def reset!
@ -460,8 +512,8 @@ EOF
# By default, a single session is automatically created for you, but you # 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 # can use this method to open multiple sessions that ought to be tested
# simultaneously. # simultaneously.
def open_session def open_session(application = nil)
session = Integration::Session.new session = Integration::Session.new(application)
# delegate the fixture accessors back to the test instance # delegate the fixture accessors back to the test instance
extras = Module.new { attr_accessor :delegate, :test_result } extras = Module.new { attr_accessor :delegate, :test_result }
@ -469,12 +521,16 @@ EOF
self.class.fixture_table_names.each do |table_name| self.class.fixture_table_names.each do |table_name|
name = table_name.tr(".", "_") name = table_name.tr(".", "_")
next unless respond_to?(name) 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
end end
# delegate add_assertion to the test case # 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.extend(extras)
session.delegate = self session.delegate = self
session.test_result = @_result session.test_result = @_result
@ -495,9 +551,13 @@ EOF
# Delegate unhandled messages to the current session instance. # Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block) def method_missing(sym, *args, &block)
reset! unless @integration_session reset! unless @integration_session
if @integration_session.respond_to?(sym)
returning @integration_session.__send__(sym, *args, &block) do returning @integration_session.__send__(sym, *args, &block) do
copy_session_variables! copy_session_variables!
end end
else
super
end
end end
end end
end end
@ -602,7 +662,8 @@ EOF
# would potentially have to set their values for both Test::Unit::TestCase # would potentially have to set their values for both Test::Unit::TestCase
# ActionController::IntegrationTest, since by the time the value is set on # ActionController::IntegrationTest, since by the time the value is set on
# TestCase, IntegrationTest has already been defined and cannot inherit # 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 class << self
def use_transactional_fixtures=(flag) #:nodoc: def use_transactional_fixtures=(flag) #:nodoc:

View file

@ -172,16 +172,8 @@ module ActionController #:nodoc:
@layout_conditions ||= read_inheritable_attribute(:layout_conditions) @layout_conditions ||= read_inheritable_attribute(:layout_conditions)
end 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: def layout_list #:nodoc:
Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] } Array(view_paths).sum([]) { |path| Dir["#{path.to_str}/layouts/**/*"] }
end end
private private
@ -200,45 +192,43 @@ module ActionController #:nodoc:
def normalize_conditions(conditions) def normalize_conditions(conditions)
conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})} conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
end end
end
def default_layout_with_format(format, layout) def initialize(*args)
list = layout_list super
if list.grep(%r{layouts/#{layout}\.#{format}(\.[a-z][0-9a-z]*)+$}).empty? @real_format = nil
(!list.grep(%r{layouts/#{layout}\.([a-z][0-9a-z]*)+$}).empty? && format == :html) ? layout : nil
else
layout
end
end
end end
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method # 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 # 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 # 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. # weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
def active_layout(passed_layout = nil) def active_layout(passed_layout = nil, options = {})
layout = passed_layout || self.class.default_layout(default_template_format) layout = passed_layout || default_layout
return layout if layout.respond_to?(:render)
active_layout = case layout active_layout = case layout
when String then layout
when Symbol then __send__(layout) when Symbol then __send__(layout)
when Proc then layout.call(self) when Proc then layout.call(self)
else layout
end end
# Explicitly passed layout names with slashes are looked up relative to the template root, find_layout(active_layout, default_template_format, options[:html_fallback]) if active_layout
# 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
end end
private private
def candidate_for_layout?(options) def default_layout #:nodoc:
options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? && layout = self.class.read_inheritable_attribute(:layout)
!@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action])) 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 end
def pick_layout(options) def pick_layout(options)
@ -247,9 +237,9 @@ module ActionController #:nodoc:
when FalseClass when FalseClass
nil nil
when NilClass, TrueClass 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 else
active_layout(layout) active_layout(layout, :html_fallback => true)
end end
else else
active_layout if action_has_layout? && candidate_for_layout?(options) active_layout if action_has_layout? && candidate_for_layout?(options)
@ -271,14 +261,26 @@ module ActionController #:nodoc:
end end
end end
def layout_directory?(layout_name) def candidate_for_layout?(options)
@template.__send__(:_pick_template, "#{File.join('layouts', layout_name)}.#{@template.template_format}") ? true : false 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 rescue ActionView::MissingTemplate
false false
end end
def default_template_format def default_template_format
response.template.template_format @real_format || response.template.template_format
end end
end 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

@ -144,11 +144,26 @@ module ActionController #:nodoc:
end end
end end
def method_missing(symbol, &block) def self.generate_method_for_mime(mime)
mime_constant = symbol.to_s.upcase 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
if Mime::SET.include?(Mime.const_get(mime_constant)) Mime::SET.each do |mime|
custom(Mime.const_get(mime_constant), &block) generate_method_for_mime(mime)
end
def method_missing(symbol, &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 else
super super
end end

View file

@ -176,6 +176,14 @@ module Mime
end end
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 # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
# ActionController::RequestForgeryProtection. # ActionController::RequestForgeryProtection.
def verify_request? 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/performance'
require 'active_support/testing/default' require 'active_support/testing/default'

View file

@ -36,12 +36,11 @@ module ActionController
# #
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt> # * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
# * <tt>new_polymorphic_url</tt>, <tt>new_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: # Example usage:
# #
# edit_polymorphic_path(@post) # => "/posts/1/edit" # 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 module PolymorphicRoutes
# Constructs a call to a named RESTful route for the given record and returns the # Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example: # resulting URL string. For example:
@ -55,7 +54,7 @@ module ActionController
# ==== Options # ==== Options
# #
# * <tt>:action</tt> - Specifies the action prefix for the named route: # * <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>. # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>. # Default is <tt>:url</tt>.
# #
@ -78,8 +77,6 @@ module ActionController
end end
record = extract_record(record_or_hash_or_array) 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)
args = case record_or_hash_or_array args = case record_or_hash_or_array
when Hash; [ record_or_hash_or_array ] when Hash; [ record_or_hash_or_array ]
@ -100,11 +97,9 @@ module ActionController
end end
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} 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, inflection, options)
named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) url_options = options.except(:action, :routing_type)
url_options = options.except(:action, :routing_type, :format)
unless url_options.empty? unless url_options.empty?
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end end
@ -119,28 +114,44 @@ module ActionController
polymorphic_url(record_or_hash_or_array, options) polymorphic_url(record_or_hash_or_array, options)
end end
%w(edit new formatted).each do |action| %w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ module_eval <<-EOT, __FILE__, __LINE__
def #{action}_polymorphic_url(record_or_hash, options = {}) def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
polymorphic_url(record_or_hash, options.merge(:action => "#{action}")) 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 end
def #{action}_polymorphic_path(record_or_hash, options = {}) def formatted_polymorphic_url(record_or_hash, options = {})
polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path)) 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 end
EOT
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 end
private private
def action_prefix(options) def action_prefix(options)
options[:action] ? "#{options[:action]}_" : options[:format] ? "formatted_" : "" options[:action] ? "#{options[:action]}_" : ''
end end
def routing_type(options) def routing_type(options)
options[:routing_type] || :url options[:routing_type] || :url
end end
def build_named_route_call(records, namespace, inflection, options = {}) def build_named_route_call(records, inflection, options = {})
unless records.is_a?(Array) unless records.is_a?(Array)
record = extract_record(records) record = extract_record(records)
route = '' route = ''
@ -150,7 +161,8 @@ module ActionController
if parent.is_a?(Symbol) || parent.is_a?(String) if parent.is_a?(Symbol) || parent.is_a?(String)
string << "#{parent}_" string << "#{parent}_"
else else
string << "#{RecordIdentifier.__send__("singular_class_name", parent)}_" string << RecordIdentifier.__send__("plural_class_name", parent).singularize
string << "_"
end end
end end
end end
@ -158,10 +170,12 @@ module ActionController
if record.is_a?(Symbol) || record.is_a?(String) if record.is_a?(Symbol) || record.is_a?(String)
route << "#{record}_" route << "#{record}_"
else else
route << "#{RecordIdentifier.__send__("#{inflection}_class_name", record)}_" route << RecordIdentifier.__send__("plural_class_name", record)
route = route.singularize if inflection == :singular
route << "_"
end end
action_prefix(options) + namespace + route + routing_type(options).to_s action_prefix(options) + route + routing_type(options).to_s
end end
def extract_record(record_or_hash_or_array) def extract_record(record_or_hash_or_array)
@ -171,28 +185,5 @@ module ActionController
else record_or_hash_or_array else record_or_hash_or_array
end end
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
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 'strscan'
require 'active_support/memoizable' require 'active_support/memoizable'
require 'action_controller/cgi_ext'
module ActionController module ActionController
# CgiRequest and TestRequest provide concrete implementations. class Request < Rack::Request
class AbstractRequest
extend ActiveSupport::Memoizable
def self.relative_url_root=(relative_url_root) %w[ AUTH_TYPE GATEWAY_INTERFACE
ActiveSupport::Deprecation.warn( PATH_TRANSLATED REMOTE_HOST
"ActionController::AbstractRequest.relative_url_root= has been renamed." + REMOTE_IDENT REMOTE_USER REMOTE_ADDR
"You can now set it with config.action_controller.relative_url_root=", caller) SERVER_NAME SERVER_PROTOCOL
ActionController::Base.relative_url_root=relative_url_root
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 end
HTTP_METHODS = %w(get head put post delete options) 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 } 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, # Returns the true HTTP request \method as a lowercase symbol, such as
# such as { 'RAILS_ENV' => 'production' }. # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS
attr_reader :env # constant above, an UnknownHttpMethod exception is raised.
# The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
def request_method def request_method
method = @env['REQUEST_METHOD'] @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
end end
memoize :request_method
# The HTTP request \method as a lowercase symbol, such as <tt>:get</tt>. # Returns the HTTP request \method used for action processing as a
# Note, HEAD is returned as <tt>:get</tt> since the two are functionally # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
# equivalent from the application's perspective. # method returns <tt>:get</tt> for a HEAD request because the two are
# functionally equivalent from the application's perspective.)
def method def method
request_method == :head ? :get : request_method request_method == :head ? :get : request_method
end end
@ -70,27 +73,35 @@ module ActionController
# #
# request.headers["Content-Type"] # => "text/plain" # request.headers["Content-Type"] # => "text/plain"
def headers def headers
ActionController::Http::Headers.new(@env) @headers ||= ActionController::Http::Headers.new(@env)
end end
memoize :headers
# Returns the content length of the request as an integer. # Returns the content length of the request as an integer.
def content_length def content_length
@env['CONTENT_LENGTH'].to_i super.to_i
end end
memoize :content_length
# The MIME type of the HTTP request, such as Mime::XML. # The MIME type of the HTTP request, such as Mime::XML.
# #
# For backward compatibility, the post \format is extracted from the # For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present. # X-Post-Data-Format HTTP header if present.
def content_type 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 end
memoize :content_type
# Returns the accepted MIME type for the request. # Returns the accepted MIME type for the request.
def accepts def accepts
@accepts ||= begin
header = @env['HTTP_ACCEPT'].to_s.strip header = @env['HTTP_ACCEPT'].to_s.strip
if header.empty? if header.empty?
@ -99,14 +110,13 @@ module ActionController
Mime::Type.parse(header) Mime::Type.parse(header)
end end
end end
memoize :accepts end
def if_modified_since def if_modified_since
if since = env['HTTP_IF_MODIFIED_SINCE'] if since = env['HTTP_IF_MODIFIED_SINCE']
Time.rfc2822(since) rescue nil Time.rfc2822(since) rescue nil
end end
end end
memoize :if_modified_since
def if_none_match def if_none_match
env['HTTP_IF_NONE_MATCH'] env['HTTP_IF_NONE_MATCH']
@ -209,7 +219,7 @@ module ActionController
# delimited list in the case of multiple chained proxies; the last # delimited list in the case of multiple chained proxies; the last
# address which is not trusted is the originating IP. # address which is not trusted is the originating IP.
def remote_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? unless remote_addr_list.blank?
not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES} 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(',') remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
if @env.include? 'HTTP_CLIENT_IP' 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 # We don't know which came from the proxy, and which from the user
raise ActionControllerError.new(<<EOM) raise ActionControllerError.new(<<EOM)
IP spoofing attack?! IP spoofing attack?!
@ -240,26 +250,21 @@ EOM
@env['REMOTE_ADDR'] @env['REMOTE_ADDR']
end end
memoize :remote_ip
# Returns the lowercase name of the HTTP server software. # Returns the lowercase name of the HTTP server software.
def server_software def server_software
(@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
end end
memoize :server_software
# Returns the complete URL used for this request. # Returns the complete URL used for this request.
def url def url
protocol + host_with_port + request_uri protocol + host_with_port + request_uri
end end
memoize :url
# Returns 'https://' if this is an SSL request and 'http://' otherwise. # Returns 'https://' if this is an SSL request and 'http://' otherwise.
def protocol def protocol
ssl? ? 'https://' : 'http://' ssl? ? 'https://' : 'http://'
end end
memoize :protocol
# Is this an SSL request? # Is this an SSL request?
def ssl? def ssl?
@ -271,7 +276,7 @@ EOM
if forwarded = env["HTTP_X_FORWARDED_HOST"] if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last forwarded.split(/,\s?/).last
else 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
end end
@ -279,14 +284,12 @@ EOM
def host def host
raw_host_with_port.sub(/:\d+$/, '') raw_host_with_port.sub(/:\d+$/, '')
end end
memoize :host
# Returns a \host:\port string for this request, such as "example.com" or # Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080". # "example.com:8080".
def host_with_port def host_with_port
"#{host}#{port_string}" "#{host}#{port_string}"
end end
memoize :host_with_port
# Returns the port number of this request as an integer. # Returns the port number of this request as an integer.
def port def port
@ -296,7 +299,6 @@ EOM
standard_port standard_port
end end
end end
memoize :port
# Returns the standard \port number for this request's protocol. # Returns the standard \port number for this request's protocol.
def standard_port def standard_port
@ -332,13 +334,8 @@ EOM
# Returns the query string, accounting for server idiosyncrasies. # Returns the query string, accounting for server idiosyncrasies.
def query_string def query_string
if uri = @env['REQUEST_URI'] @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
uri.split('?', 2)[1] || ''
else
@env['QUERY_STRING'] || ''
end end
end
memoize :query_string
# Returns the request URI, accounting for server idiosyncrasies. # Returns the request URI, accounting for server idiosyncrasies.
# WEBrick includes the full URL. IIS leaves REQUEST_URI blank. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
@ -364,36 +361,33 @@ EOM
end end
end end
end end
memoize :request_uri
# Returns the interpreted \path to requested resource after all the installation # Returns the interpreted \path to requested resource after all the installation
# directory of this application was taken into account. # directory of this application was taken into account.
def path def path
path = (uri = request_uri) ? uri.split('?').first.to_s : '' path = request_uri.to_s[/\A[^\?]*/]
path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
# Cut off the path to the installation directory if given path
path.sub!(%r/^#{ActionController::Base.relative_url_root}/, '')
path || ''
end end
memoize :path
# Read the request \body. This is useful for web services that need to # Read the request \body. This is useful for web services that need to
# work with raw requests directly. # work with raw requests directly.
def raw_post def raw_post
unless env.include? 'RAW_POST_DATA' unless @env.include? 'RAW_POST_DATA'
env['RAW_POST_DATA'] = body.read(content_length) @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
body.rewind if body.respond_to?(:rewind) body.rewind if body.respond_to?(:rewind)
end end
env['RAW_POST_DATA'] @env['RAW_POST_DATA']
end end
# Returns both GET and POST \parameters in a single hash. # Returns both GET and POST \parameters in a single hash.
def parameters def parameters
@parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
end end
alias_method :params, :parameters
def path_parameters=(parameters) #:nodoc: def path_parameters=(parameters) #:nodoc:
@path_parameters = parameters @env["action_controller.request.path_parameters"] = parameters
@symbolized_path_parameters = @parameters = nil @symbolized_path_parameters = @parameters = nil
end end
@ -409,464 +403,91 @@ EOM
# #
# See <tt>symbolized_path_parameters</tt> for symbolized keys. # See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters def path_parameters
@path_parameters ||= {} @env["action_controller.request.path_parameters"] ||= {}
end end
# The request body is an IO input stream. If the RAW_POST_DATA environment # The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO. # variable is already set, wrap it in a StringIO.
def body 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) raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
StringIO.new(raw_post) StringIO.new(raw_post)
else else
body_stream @env['rack.input']
end end
end end
def remote_addr def form_data?
@env['REMOTE_ADDR'] FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
end end
def referrer # Override Rack's GET method to support indifferent access
@env['HTTP_REFERER'] def GET
@env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
end end
alias referer referrer alias_method :query_parameters, :GET
# Override Rack's POST method to support indifferent access
def query_parameters def POST
@query_parameters ||= self.class.parse_query_parameters(query_string) @env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
end end
alias_method :request_parameters, :POST
def request_parameters
@request_parameters ||= parse_formatted_request_parameters
end
#--
# Must be implemented in the concrete request
#++
def body_stream #:nodoc: def body_stream #:nodoc:
@env['rack.input']
end end
def cookies #:nodoc: def session
end @env['rack.session'] ||= {}
def session #:nodoc:
end end
def session=(session) #:nodoc: def session=(session) #:nodoc:
@session = session @env['rack.session'] = session
end end
def reset_session #:nodoc: def reset_session
@env['rack.session.options'].delete(:id)
@env['rack.session'] = {}
end end
protected def session_options
# The raw content type string. Use when you need parameters such as @env['rack.session.options'] ||= {}
# 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 end
# The raw content type string with its parameters stripped off. def session_options=(options)
def content_type_without_parameters @env['rack.session.options'] = options
self.class.extract_content_type_without_parameters(content_type_with_parameters) end
def server_port
@env['SERVER_PORT'].to_i
end end
memoize :content_type_without_parameters
private 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) def named_host?(host)
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
end end
class << self # Convert nested Hashs to HashWithIndifferentAccess and replace
def parse_query_parameters(query_string) # file upload hashs with UploadedFile objects
return {} if query_string.blank? def normalize_parameters(value)
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 case value
when String when Hash
value if value.has_key?(:tempfile)
when NilClass upload = value[:tempfile]
'' upload.extend(UploadedFile)
upload.original_path = value[:filename]
upload.content_type = value[:type]
upload
else
h = {}
value.each { |k, v| h[k] = normalize_parameters(v) }
h.with_indifferent_access
end
when Array when Array
value.map { |v| get_typed_value(v) } value.map { |e| normalize_parameters(e) }
else else
if value.respond_to? :original_filename
# Uploaded file
if value.original_filename
value 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 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
else
top << value
end
elsif top.is_a? Hash
key = CGI.unescape(key)
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
top[key] ||= value
return top[key]
else
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
end
return value
end
def type_conflict!(klass, value)
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
end
end
module UploadedFile
def self.included(base)
base.class_eval do
attr_accessor :original_path, :content_type
alias_method :local_path, :path
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 end

View file

@ -5,8 +5,6 @@ module ActionController #:nodoc:
module RequestForgeryProtection module RequestForgeryProtection
def self.included(base) def self.included(base)
base.class_eval do base.class_eval do
class_inheritable_accessor :request_forgery_protection_options
self.request_forgery_protection_options = {}
helper_method :form_authenticity_token helper_method :form_authenticity_token
helper_method :protect_against_forgery? helper_method :protect_against_forgery?
end end
@ -14,7 +12,7 @@ module ActionController #:nodoc:
end end
# Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current web application, not a # 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 # 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 # 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. # scheme there anyway). Also, GET requests are not protected as these should be idempotent anyway.
@ -57,12 +55,8 @@ module ActionController #:nodoc:
# Example: # Example:
# #
# class FooController < ApplicationController # class FooController < ApplicationController
# # uses the cookie session store (then you don't need a separate :secret)
# protect_from_forgery :except => :index # 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: # # you can disable csrf protection on controller-by-controller basis:
# skip_before_filter :verify_authenticity_token # skip_before_filter :verify_authenticity_token
# end # end
@ -70,13 +64,12 @@ module ActionController #:nodoc:
# Valid Options: # Valid Options:
# #
# * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. # * <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 = {}) def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token self.request_forgery_protection_token ||= :authenticity_token
before_filter :verify_authenticity_token, :only => options.delete(:only), :except => options.delete(:except) 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
end end
@ -88,14 +81,19 @@ module ActionController #:nodoc:
# Returns true or false if a request is verified. Checks: # 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 # * 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? def verified_request?
!protect_against_forgery? || !protect_against_forgery? ||
request.method == :get || request.method == :get ||
request.xhr? ||
!verifiable_request_format? || !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 end
def verifiable_request_format? def verifiable_request_format?
@ -105,32 +103,7 @@ module ActionController #:nodoc:
# Sets the token value for the current session. Pass a <tt>:secret</tt> option # 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. # in +protect_from_forgery+ to add a custom salt to the hash.
def form_authenticity_token def form_authenticity_token
@form_authenticity_token ||= if !session.respond_to?(:session_id) session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
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
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 end
def protect_against_forgery? def protect_against_forgery?

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: module ActionController #:nodoc:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view # Actions that fail to perform as expected throw exceptions. These
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view # exceptions can either be rescued for the public view (with a nice
# is already implemented by the Action Controller, but the public view should be tailored to your specific application. # 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 # The default behavior for public exceptions is to render a static html
# file exists, an empty response is sent with the correct status code. # 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. # You can override what constitutes a local request by overriding the
# Custom rescue behavior is achieved by overriding the <tt>rescue_action_in_public</tt> and <tt>rescue_action_locally</tt> methods. # <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 module Rescue
LOCALHOST = '127.0.0.1'.freeze LOCALHOST = '127.0.0.1'.freeze
@ -32,6 +38,9 @@ module ActionController #:nodoc:
'ActionView::TemplateError' => 'template_error' 'ActionView::TemplateError' => 'template_error'
} }
RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new_and_loaded(
File.join(File.dirname(__FILE__), "templates"))
def self.included(base) #:nodoc: def self.included(base) #:nodoc:
base.cattr_accessor :rescue_responses base.cattr_accessor :rescue_responses
base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE) base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
@ -50,47 +59,60 @@ module ActionController #:nodoc:
end end
module ClassMethods 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) new.process(request, response, :rescue_action, exception)
end end
end end
protected 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) def rescue_action(exception)
rescue_with_handler(exception) || rescue_action_without_handler(exception) rescue_with_handler(exception) ||
rescue_action_without_handler(exception)
end 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: def log_error(exception) #:doc:
ActiveSupport::Deprecation.silence do ActiveSupport::Deprecation.silence do
if ActionView::TemplateError === exception if ActionView::TemplateError === exception
logger.fatal(exception.to_s) logger.fatal(exception.to_s)
else else
logger.fatal( logger.fatal(
"\n\n#{exception.class} (#{exception.message}):\n " + "\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") + clean_backtrace(exception).join("\n ") + "\n\n"
"\n\n"
) )
end end
end end
end end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). By # Overwrite to implement public exception handling (for requests
# default will call render_optional_error_file. Override this method to provide more user friendly error messages. # 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: def rescue_action_in_public(exception) #:doc:
render_optional_error_file response_code_for_rescue(exception) render_optional_error_file response_code_for_rescue(exception)
end end
# Attempts to render a static error page based on the <tt>status_code</tt> thrown, # Attempts to render a static error page based on the
# or just return headers if no such file exists. For example, if a 500 error is # <tt>status_code</tt> thrown, or just return headers if no such file
# being handled Rails will first attempt to render the file at <tt>public/500.html</tt>. # exists. At first, it will try to render a localized static page.
# If the file doesn't exist, the body of the response will be left empty. # 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) def render_optional_error_file(status_code)
status = interpret_status(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" 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 else
head status head status
end end
@ -107,11 +129,13 @@ module ActionController #:nodoc:
# a controller action. # a controller action.
def rescue_action_locally(exception) def rescue_action_locally(exception)
@template.instance_variable_set("@exception", exception) @template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub"))) @template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH)
@template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception))) @template.instance_variable_set("@contents",
@template.render(:file => template_path_for_local_rescue(exception)))
response.content_type = Mime::HTML 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 end
def rescue_action_without_handler(exception) def rescue_action_without_handler(exception)
@ -139,7 +163,7 @@ module ActionController #:nodoc:
end end
def rescues_path(template_name) def rescues_path(template_name)
"#{File.dirname(__FILE__)}/templates/rescues/#{template_name}.erb" RESCUES_TEMPLATE_PATH["rescues/#{template_name}.erb"]
end end
def template_path_for_local_rescue(exception) def template_path_for_local_rescue(exception)
@ -151,13 +175,9 @@ module ActionController #:nodoc:
end end
def clean_backtrace(exception) def clean_backtrace(exception)
if backtrace = exception.backtrace defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
if defined?(RAILS_ROOT) Rails.backtrace_cleaner.clean(exception.backtrace) :
backtrace.map { |line| line.sub RAILS_ROOT, '' } exception.backtrace
else
backtrace
end
end
end end
end end
end end

View file

@ -42,7 +42,7 @@ module ActionController
# #
# Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer
module Resources module Resources
INHERITABLE_OPTIONS = :namespace, :shallow, :actions INHERITABLE_OPTIONS = :namespace, :shallow
class Resource #:nodoc: class Resource #:nodoc:
DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy
@ -91,7 +91,7 @@ module ActionController
end end
def shallow_path_prefix def shallow_path_prefix
@shallow_path_prefix ||= "#{path_prefix unless @options[:shallow]}" @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix
end end
def member_path def member_path
@ -103,7 +103,7 @@ module ActionController
end end
def shallow_name_prefix def shallow_name_prefix
@shallow_name_prefix ||= "#{name_prefix unless @options[:shallow]}" @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix
end end
def nesting_name_prefix def nesting_name_prefix
@ -119,7 +119,7 @@ module ActionController
end end
def has_action?(action) def has_action?(action)
!DEFAULT_ACTIONS.include?(action) || @options[:actions].nil? || @options[:actions].include?(action) !DEFAULT_ACTIONS.include?(action) || action_allowed?(action)
end end
protected protected
@ -135,22 +135,27 @@ module ActionController
end end
def set_allowed_actions def set_allowed_actions
only = @options.delete(:only) only, except = @options.values_at(:only, :except)
except = @options.delete(:except) @allowed_actions ||= {}
if only && except if only == :all || except == :none
raise ArgumentError, 'Please supply either :only or :except, not both.' only = nil
elsif only == :all || except == :none except = []
options[:actions] = DEFAULT_ACTIONS
elsif only == :none || except == :all elsif only == :none || except == :all
options[:actions] = [] only = []
elsif only except = nil
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
end 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 end
def set_prefixes 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>: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>:controller</tt> - Specify the controller name for the routes.
# * <tt>:singular</tt> - Specify the singular name used in the member 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>: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: # * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example:
# # products_path == '/productos' # # products_path == '/productos'
@ -307,9 +317,10 @@ module ActionController
# notes.resources :attachments # notes.resources :attachments
# end # 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' # # 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: # You can also set default action names from an environment, like this:
# config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } # 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) # # --> POST /posts/1/comments (maps to the CommentsController#create action)
# # --> PUT /posts/1/comments/1 (fails) # # --> 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. # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied.
# #
# Examples: # Examples:
@ -517,16 +526,16 @@ module ActionController
resource = Resource.new(entities, options) resource = Resource.new(entities, options)
with_options :controller => resource.controller do |map| 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) map_associations(resource, options)
if block_given? if block_given?
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end end
map_collection_actions(map, resource)
map_default_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
end end
end end
@ -534,16 +543,16 @@ module ActionController
resource = SingletonResource.new(entities, options) resource = SingletonResource.new(entities, options)
with_options :controller => resource.controller do |map| 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) map_associations(resource, options)
if block_given? if block_given?
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
end end
map_collection_actions(map, resource)
map_new_actions(map, resource)
map_member_actions(map, resource)
map_default_singleton_actions(map, resource)
end end
end end
@ -578,7 +587,10 @@ module ActionController
resource.collection_methods.each do |method, actions| resource.collection_methods.each do |method, actions|
actions.each do |action| actions.each do |action|
[method].flatten.each do |m| [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 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 = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
action_path ||= Base.resources_path_names[action] || action 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 end
end end
@ -633,16 +645,14 @@ module ActionController
map_resource_routes(map, resource, :destroy, resource.member_path, route_path) map_resource_routes(map, resource, :destroy, resource.member_path, route_path)
end 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) 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" formatted_route_path = "#{route_path}.:format"
if route_name && @set.named_routes[route_name.to_sym].nil? if route_name && @set.named_routes[route_name.to_sym].nil?
map.named_route(route_name, route_path, action_options) map.named_route(route_name, formatted_route_path, action_options)
map.named_route("formatted_#{route_name}", formatted_route_path, action_options)
else else
map.connect(route_path, action_options)
map.connect(formatted_route_path, action_options) map.connect(formatted_route_path, action_options)
end end
end end
@ -654,9 +664,10 @@ module ActionController
end end
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 } default_options = { :action => action.to_s }
require_id = !resource.kind_of?(SingletonResource) require_id = !resource.kind_of?(SingletonResource)
force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource)
case default_options[:action] case default_options[:action]
when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) 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 "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 "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)) 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
end end
end end
class ActionController::Routing::RouteSet::Mapper
include ActionController::Resources
end

View file

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

View file

@ -1,6 +1,5 @@
require 'cgi' require 'cgi'
require 'uri' require 'uri'
require 'action_controller/polymorphic_routes'
require 'action_controller/routing/optimisations' require 'action_controller/routing/optimisations'
require 'action_controller/routing/routing_ext' require 'action_controller/routing/routing_ext'
require 'action_controller/routing/route' 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 sets up +blog+ as the default controller if no other is specified.
# This means visiting '/' would invoke the blog controller. # 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 # 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 # 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?' # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
# #
# will glob all remaining parts of the route that were not recognized earlier. This idiom # will glob all remaining parts of the route that were not recognized earlier.
# must appear at the end of the path. The globbed values are in <tt>params[:path]</tt> in # The globbed values are in <tt>params[:path]</tt> as an array of path segments.
# this case.
# #
# == Route conditions # == Route conditions
# #
@ -267,10 +267,13 @@ module ActionController
module Routing module Routing
SEPARATORS = %w( / . ? ) 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 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 # The root paths which may contain controller files
mattr_accessor :controller_paths mattr_accessor :controller_paths
self.controller_paths = [] self.controller_paths = []

View file

@ -34,6 +34,8 @@ module ActionController
def segment_for(string) def segment_for(string)
segment = segment =
case string case string
when /\A\.(:format)?\//
OptionalFormatSegment.new
when /\A:(\w+)/ when /\A:(\w+)/
key = $1.to_sym key = $1.to_sym
key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key) key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key)
@ -157,7 +159,8 @@ module ActionController
path = "/#{path}" unless path[0] == ?/ path = "/#{path}" unless path[0] == ?/
path = "#{path}/" unless path[-1] == ?/ 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) segments = segments_for_route_path(path)
defaults, requirements, conditions = divide_route_options(segments, options) 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+. # rather than triggering the expensive logic in +url_for+.
class PositionalArguments < Optimiser class PositionalArguments < Optimiser
def guard_conditions 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 # if they're using foo_url(:id=>2) it's one
# argument, but we don't want to generate /foos/id2 # argument, but we don't want to generate /foos/id2
if number_of_arguments == 1 if number_of_arguments == 1

View file

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

View file

@ -36,6 +36,11 @@ module ActionController
end.compact end.compact
end 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+ # 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 # is given (as an array), only the keys indicated will be used to build
# the query string. The query string will correctly build array parameter # the query string. The query string will correctly build array parameter
@ -122,6 +127,16 @@ module ActionController
super super
end 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 private
def requirement_for(key) def requirement_for(key)
return requirements[key] if requirements.key? 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 # the query string. (Never use keys from the recalled request when building the
# query string.) # 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 raw_method
end end

View file

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

View file

@ -3,7 +3,11 @@ module ActionController
class Segment #:nodoc: class Segment #:nodoc:
RESERVED_PCHAR = ':@&=+$,;' RESERVED_PCHAR = ':@&=+$,;'
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
if RUBY_VERSION >= '1.9'
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
else
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
end
# TODO: Convert :is_optional accessor to read only # TODO: Convert :is_optional accessor to read only
attr_accessor :is_optional attr_accessor :is_optional
@ -191,23 +195,19 @@ module ActionController
end end
def regexp_chunk def regexp_chunk
if regexp regexp ? regexp_string : default_regexp_chunk
if regexp_has_modifiers?
"(#{regexp.to_s})"
else
"(#{regexp.source})"
end end
else
def regexp_string
regexp_has_modifiers? ? "(#{regexp.to_s})" : "(#{regexp.source})"
end
def default_regexp_chunk
"([^#{Routing::SEPARATORS.join}]+)" "([^#{Routing::SEPARATORS.join}]+)"
end end
end
def number_of_captures def number_of_captures
if regexp regexp ? regexp.number_of_captures + 1 : 1
regexp.number_of_captures + 1
else
1
end
end end
def build_pattern(pattern) def build_pattern(pattern)
@ -244,10 +244,6 @@ module ActionController
"(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
end end
def number_of_captures
1
end
# Don't URI.escape the controller name since it may contain slashes. # Don't URI.escape the controller name since it may contain slashes.
def interpolation_chunk(value_code = local_name) def interpolation_chunk(value_code = local_name)
"\#{#{value_code}.to_s}" "\#{#{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}" "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
end end
def regexp_chunk def default_regexp_chunk
regexp || "(.*)" "(.*)"
end end
def number_of_captures def number_of_captures
@ -308,5 +304,40 @@ module ActionController
end end
end 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
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,7 +1,5 @@
require 'cgi' module ActionController
require 'cgi/session' module Session
require 'openssl' # to generate the HMAC message digest
# This cookie-based session store is the Rails default. Sessions typically # 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 # 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 # size limit. Cookie-based sessions are dramatically faster than the
@ -11,20 +9,20 @@ require 'openssl' # to generate the HMAC message digest
# visible to the user, pick another session store. # visible to the user, pick another session store.
# #
# CookieOverflow is raised if you attempt to store more than 4K of data. # 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 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 # a user cannot alter his +user_id+ without knowing the secret key
# the hash. New apps are generated with a pregenerated secret in # included in the hash. New apps are generated with a pregenerated secret
# config/environment.rb. Set your own for old apps you're upgrading. # in config/environment.rb. Set your own for old apps you're upgrading.
# #
# Session options: # Session options:
# #
# * <tt>:secret</tt>: An application-wide key string or block returning a string # * <tt>:secret</tt>: An application-wide key string or block returning a
# called per generated digest. The block is called with the CGI::Session # string called per generated digest. The block is called with the
# instance as an argument. It's important that the secret is not vulnerable to # CGI::Session instance as an argument. It's important that the secret
# a dictionary attack. Therefore, you should choose a secret consisting of # is not vulnerable to a dictionary attack. Therefore, you should choose
# random numbers and letters and more than 30 characters. Examples: # a secret consisting of random numbers and letters and more than 30
# characters. Examples:
# #
# :secret => '449fe2e7daee471bffae2fd8dc02313d' # :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key } # :secret => Proc.new { User.current_user.secret_key }
@ -37,47 +35,137 @@ require 'openssl' # to generate the HMAC message digest
# "rake secret" and set the key in config/environment.rb. # "rake secret" and set the key in config/environment.rb.
# #
# Note that changing digest or secret invalidates all existing sessions! # Note that changing digest or secret invalidates all existing sessions!
class CGI::Session::CookieStore class CookieStore
# Cookies can typically store 4096 bytes. # Cookies can typically store 4096 bytes.
MAX = 4096 MAX = 4096
SECRET_MIN_LENGTH = 30 # characters SECRET_MIN_LENGTH = 30 # characters
DEFAULT_OPTIONS = {
:key => '_session_id',
:domain => nil,
:path => "/",
:expire_after => nil,
:httponly => true
}.freeze
ENV_SESSION_KEY = "rack.session".freeze
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
HTTP_SET_COOKIE = "Set-Cookie".freeze
# Raised when storing more than 4K of session data. # Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end class CookieOverflow < StandardError; end
# Raised when the cookie fails its integrity check. def initialize(app, options = {})
class TamperedWithCookie < StandardError; end # Process legacy CGI options
options = options.symbolize_keys
# Called from CGI::Session only. if options.has_key?(:session_path)
def initialize(session, options = {}) options[:path] = options.delete(:session_path)
# The session_key option is required. end
if options['session_key'].blank? if options.has_key?(:session_key)
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' options[:key] = options.delete(:session_key)
end
if options.has_key?(:session_http_only)
options[:httponly] = options.delete(:session_http_only)
end end
@app = app
# The session_key option is required.
ensure_session_key(options[:key])
@key = options.delete(:key).freeze
# The secret option is required. # The secret option is required.
ensure_secret_secure(options['secret']) ensure_secret_secure(options[:secret])
@secret = options.delete(:secret).freeze
# Keep the session and its secret on hand so we can read and write cookies. @digest = options.delete(:digest) || 'SHA1'
@session, @secret = session, options['secret'] @verifier = verifier_for(@secret, @digest)
# Message digest defaults to SHA1. @default_options = DEFAULT_OPTIONS.merge(options).freeze
@digest = options['digest'] || 'SHA1'
# Default cookie options derived from session settings. freeze
@cookie_options = { end
'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 def call(env)
# set our own data cookie. env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
options['no_hidden'] = true env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
options['no_cookies'] = true
status, headers, body = @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?)
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 end
# To prevent users from using something insecure like "Password" we make sure that the # To prevent users from using something insecure like "Password" we make sure that the
@ -88,80 +176,46 @@ class CGI::Session::CookieStore
return true if secret.is_a?(Proc) return true if secret.is_a?(Proc)
if secret.blank? 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} 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 end
if secret.length < SECRET_MIN_LENGTH 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} 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
end end
# Restore session data from the cookie. def verifier_for(secret, digest)
def restore key = secret.respond_to?(:call) ? secret.call : secret
@original = read_cookie ActiveSupport::MessageVerifier.new(key, digest)
@data = unmarshal(@original) || {}
end end
# Wait until close to write the session data cookie. def generate_sid
def update; end ActiveSupport::SecureRandom.hex(16)
# 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 end
# Delete the session data by setting an expired cookie with no data. def persistent_session_id!(data)
def delete (data ||= {}).merge!(inject_persistent_session_id(data))
@data = nil
clear_old_cookie_value
write_cookie('value' => nil, 'expires' => 1.year.ago)
end end
# Generate the HMAC keyed message digest. Uses SHA1 by default. def inject_persistent_session_id(data)
def generate_digest(data) requires_session_id?(data) ? { :session_id => generate_sid } : {}
key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
end end
private def requires_session_id?(data)
# Marshal a session hash into safe cookie data. Include an integrity hash. if data
def marshal(session) data.respond_to?(:key?) && !data.key?(:session_id)
data = ActiveSupport::Base64.encode64s(Marshal.dump(session)) else
"#{data}--#{generate_digest(data)}" true
end 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
end
Marshal.load(ActiveSupport::Base64.decode64(data))
end end
end end
# Read the session data cookie.
def read_cookie
@session.cgi.cookies[@cookie_options['name']].first
end
# CGI likes to make you hack.
def write_cookie(options)
cookie = CGI::Cookie.new(@cookie_options.merge(options))
@session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
end
# Clear cookie value so subsequent new_session doesn't reload old data.
def clear_old_cookie_value
@session.cgi.cookies[@cookie_options['name']].clear
end end
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 begin
require 'cgi/session'
require_library_or_gem 'memcache' require_library_or_gem 'memcache'
class CGI module ActionController
class Session module Session
# MemCache-based session storage class. class MemCacheStore < AbstractStore
# def initialize(app, options = {})
# This builds upon the top-level MemCache class provided by the # Support old :expires option
# library file memcache.rb. Session data is marshalled and stored options[:expire_after] ||= options[:expires]
# in a memcached cache.
class MemCacheStore super
def check_id(id) #:nodoc:#
/[^0-9a-zA-Z]+/ =~ id.to_s ? false : true @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
@mutex = Mutex.new
super
end end
# Create a new CGI::Session::MemCache instance private
# def get_session(env, sid)
# This constructor is used internally by CGI::Session. The sid ||= generate_sid
# user does not generally need to call it directly. begin
# session = @pool.get(sid) || {}
# +session+ is the session for which this instance is being rescue MemCache::MemCacheError, Errno::ECONNREFUSED
# created. The session id must only contain alphanumeric session = {}
# 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
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)
end end
[sid, session]
end end
# Restore session state from the session's memcache entry. def set_session(env, sid, session_data)
# options = env['rack.session.options']
# Returns the session state as a hash. expiry = options[:expire_after] || 0
def restore @pool.set(sid, session_data, expiry)
@session_data = @cache[@session_key] || {} return true
rescue MemCache::MemCacheError, Errno::ECONNREFUSED
return false
end 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
end end
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 ActionController #:nodoc:
module SessionManagement #:nodoc: module SessionManagement #:nodoc:
def self.included(base) def self.included(base)
base.class_eval do base.class_eval do
extend ClassMethods extend ClassMethods
alias_method_chain :process, :session_management_support
alias_method_chain :process_cleanup, :session_management_support
end end
end end
@ -19,143 +10,44 @@ module ActionController #:nodoc:
# Set the session store to be used for keeping the session data between requests. # 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>), # 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>, # 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>:mem_cache_store</tt>, or your own custom class.
# <tt>:memory_store</tt>) or your own custom class.
def session_store=(store) def session_store=(store)
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] = if store == :active_record_store
store.is_a?(Symbol) ? CGI::Session.const_get(store == :drb_store ? "DRbStore" : store.to_s.camelize) : store self.session_store = ActiveRecord::SessionStore
else
@@session_store = store.is_a?(Symbol) ?
Session.const_get(store.to_s.camelize) :
store
end
end end
# Returns the session store class currently used. # Returns the session store class currently used.
def session_store 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 end
# Returns the hash used to configure the session. Example use: # 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 def session_options
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS @session_options ||= {}
end 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) def session(*args)
options = args.extract_options! ActiveSupport::Deprecation.warn(
"Disabling sessions for a single controller has been deprecated. " +
options[:disabled] = false if args.delete(:on) "Sessions are now lazy loaded. So if you don't access them, " +
options[:disabled] = true if !args.empty? "consider them off. You can still modify the session cookie " +
options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only] "options with request.session_options.", caller)
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
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
end end

View file

@ -1,5 +1,8 @@
require 'active_support/core_ext/string/bytesize'
module ActionController #:nodoc: 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 module Streaming
DEFAULT_SEND_FILE_OPTIONS = { DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet-stream'.freeze, :type => 'application/octet-stream'.freeze,
@ -24,7 +27,8 @@ module ActionController #:nodoc:
# Options: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>. # 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 # * <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>. # 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. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
@ -102,12 +106,16 @@ module ActionController #:nodoc:
end end
end end
# Send binary data to the user as a file download. May set content type, apparent file name, # Sends the given binary data to the browser. This method is similar to
# and specify whether to show data inline or download as an attachment. # <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: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * <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. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default). # Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. # * <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' # send_data image.data, :type => image.content_type, :disposition => 'inline'
# #
# See +send_file+ for more information on HTTP Content-* headers and caching. # 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: def send_data(data, options = {}) #:doc:
logger.info "Sending data #{options[:filename]}" if logger 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 @performed_render = false
render :status => options[:status], :text => data render :status => options[:status], :text => data
end end
@ -143,9 +155,16 @@ module ActionController #:nodoc:
disposition <<= %(; filename="#{options[:filename]}") if options[:filename] disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
headers.update( content_type = options[:type]
'Content-Length' => options[:length], if content_type.is_a?(Symbol)
'Content-Type' => options[:type].to_s.strip, # fixes a problem with extra '\r' with some browsers 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-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary' '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> </h1>
<pre><%=h @exception.clean_message %></pre> <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 <% @real_exception = @exception
@exception = @exception.original_exception || @exception %> @exception = @exception.original_exception || @exception %>
<%= render(:file => @rescues_path + "/_trace.erb") %> <%= render :file => @rescues_path["rescues/_trace.erb"] %>
<% @exception = @real_exception %> <% @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 'active_support/test_case'
require 'action_controller/test_process'
module ActionController 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 # Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method. This should not be confused with # test a single controller action per test method. This should not be confused with
# integration tests (see ActionController::IntegrationTest), which are more like # integration tests (see ActionController::IntegrationTest), which are more like
@ -74,7 +61,70 @@ module ActionController
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase # class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController # tests WidgetController
# end # 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 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 # 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 # (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 # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
@ -82,8 +132,14 @@ module ActionController
# #
# The exception is stored in the exception accessor for further inspection. # The exception is stored in the exception accessor for further inspection.
module RaiseActionExceptions module RaiseActionExceptions
def self.included(base)
base.class_eval do
attr_accessor :exception attr_accessor :exception
protected :exception, :exception=
end
end
protected
def rescue_action_without_handler(e) def rescue_action_without_handler(e)
self.exception = e self.exception = e
@ -107,7 +163,7 @@ module ActionController
end end
def controller_class=(new_class) 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) write_inheritable_attribute(:controller_class, new_class)
end end
@ -122,7 +178,7 @@ module ActionController
def determine_default_controller_class(name) def determine_default_controller_class(name)
name.sub(/Test$/, '').constantize name.sub(/Test$/, '').constantize
rescue NameError rescue NameError
raise NonInferrableControllerError.new(name) nil
end end
def prepare_controller_class(new_class) def prepare_controller_class(new_class)
@ -131,13 +187,19 @@ module ActionController
end end
def setup_controller_request_and_response def setup_controller_request_and_response
@controller = self.class.controller_class.new @request = TestRequest.new
@controller.request = @request = TestRequest.new
@response = TestResponse.new @response = TestResponse.new
if klass = self.class.controller_class
@controller ||= klass.new rescue nil
end
if @controller
@controller.request = @request
@controller.params = {} @controller.params = {}
@controller.send(:initialize_current_url) @controller.send(:initialize_current_url)
end end
end
# Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local # 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! def rescue_action_in_public!

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