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)*
* 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)*
* 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
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]
* 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]
@ -29,7 +50,7 @@
*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
* 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]
* 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]
@ -77,7 +98,7 @@
*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)
@ -97,7 +118,7 @@
* 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]

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
a copy of this software and associated documentation files (the

View file

@ -4,7 +4,6 @@ require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -55,7 +54,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer"
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.requirements << 'none'
@ -76,12 +75,14 @@ end
desc "Publish the API documentation"
task :pgem => [:package] do
require 'rake/contrib/sshpublisher'
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'`
end
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
require 'rake/contrib/sshpublisher'
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload
end

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004-2008 David Heinemeier Hansson
# Copyright (c) 2004-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -31,22 +31,32 @@ rescue LoadError
end
end
require 'action_mailer/vendor'
require 'tmail'
require 'action_view'
require 'action_mailer/base'
require 'action_mailer/helpers'
require 'action_mailer/mail_helper'
require 'action_mailer/quoting'
require 'action_mailer/test_helper'
module ActionMailer
def self.load_all!
[Base, Part, ::Text::Format, ::Net::SMTP]
end
require 'net/smtp'
ActionMailer::Base.class_eval do
include ActionMailer::Quoting
include ActionMailer::Helpers
helper MailHelper
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
silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) }
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:
# 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
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# bcc ["bcc@example.com", "Order Watcher <watcher@example.com>"]
# from "system@example.com"
# subject "New account information"
# 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>: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>.
# * <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>: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
# 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
# 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
# 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
# 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
# and appear last in the mime encoded message. You can also pick a different order from inside a method with
# +implicit_parts_order+.
class Base
include AdvAttrAccessor, PartContainer
include AdvAttrAccessor, PartContainer, Quoting, Utils
if Object.const_defined?(:ActionController)
include ActionController::UrlWriter
include ActionController::Layout
@ -254,15 +254,18 @@ module ActionMailer #:nodoc:
private_class_method :new #:nodoc:
class_inheritable_accessor :view_paths
self.view_paths = []
cattr_accessor :logger
@@smtp_settings = {
:address => "localhost",
:port => 25,
:domain => 'localhost.localdomain',
:user_name => nil,
:password => nil,
:authentication => nil
:address => "localhost",
:port => 25,
:domain => 'localhost.localdomain',
:user_name => nil,
:password => nil,
:authentication => nil,
:enable_starttls_auto => true,
}
cattr_accessor :smtp_settings
@ -426,12 +429,6 @@ module ActionMailer #:nodoc:
new.deliver!(mail)
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
self.view_paths && self.view_paths.first
end
@ -482,7 +479,7 @@ module ActionMailer #:nodoc:
)
end
unless @parts.empty?
@content_type = "multipart/alternative"
@content_type = "multipart/alternative" if @content_type !~ /^multipart/
@parts = sort_parts(@parts, @implicit_parts_order)
end
end
@ -546,10 +543,16 @@ module ActionMailer #:nodoc:
@headers ||= {}
@body ||= {}
@mime_version = @@default_mime_version.dup if @@default_mime_version
@sent_on ||= Time.now
end
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
ensure
@current_template_content_type = nil
end
def render(opts)
@ -568,11 +571,17 @@ module ActionMailer #:nodoc:
end
def default_template_format
:html
if @current_template_content_type
Mime::Type.lookup(@current_template_content_type).to_sym
else
:html
end
end
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
def template_root
@ -588,7 +597,9 @@ module ActionMailer #:nodoc:
end
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
def sort_parts(parts, order = [])
@ -637,11 +648,11 @@ module ActionMailer #:nodoc:
if @parts.empty?
m.set_content_type(real_content_type, nil, ctype_attrs)
m.body = Utils.normalize_new_lines(body)
m.body = normalize_new_lines(body)
else
if String === body
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_disposition "inline"
m.parts << part
@ -664,10 +675,10 @@ module ActionMailer #:nodoc:
def perform_delivery_smtp(mail)
destinations = mail.destinations
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.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_settings[:authentication]) do |smtp|
smtp.sendmail(mail.encoded, sender, destinations)
@ -687,4 +698,9 @@ module ActionMailer #:nodoc:
deliveries << mail
end
end
Base.class_eval do
include Helpers
helper MailHelper
end
end

View file

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

View file

@ -1,5 +1,3 @@
require 'text/format'
module MailHelper
# Uses Text::Format to take the text and format it, indented two spaces for
# 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
# Represents a subpart of an email message. It shares many similar
# attributes of ActionMailer::Base. Although you can create parts manually
# and add them to the +parts+ list of the mailer, it is easier
# to use the helper methods in ActionMailer::PartContainer.
class Part
include ActionMailer::AdvAttrAccessor
include ActionMailer::PartContainer
include AdvAttrAccessor, PartContainer, Utils
# 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
@ -64,7 +59,7 @@ module ActionMailer
when "base64" then
part.body = TMail::Base64.folding_encode(body)
when "quoted-printable"
part.body = [Utils.normalize_new_lines(body)].pack("M*")
part.body = [normalize_new_lines(body)].pack("M*")
else
part.body = body
end
@ -93,7 +88,10 @@ module ActionMailer
part.parts << prt
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
headers.each { |k,v| part[k] = v }
@ -102,7 +100,6 @@ module ActionMailer
end
private
def squish(values={})
values.delete_if { |k,v| v.nil? }
end

View file

@ -41,7 +41,11 @@ module ActionMailer
private
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*/)
attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h }
[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)
def quoted_printable_encode(character)
result = ""
character.each_byte { |b| result << "=%02x" % b }
character.each_byte { |b| result << "=%02X" % b }
result
end

View file

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

View file

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

View file

@ -3,6 +3,5 @@ module ActionMailer
def normalize_new_lines(text)
text.to_s.gsub(/\r\n?/, "\n")
end
module_function :normalize_new_lines
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_match(/^of freedom, and that government of the people, by the people, for the$/,
@format_o.format(GETTYSBURG).split("\n")[-3])
assert_raises(ArgumentError) { @format_o.format_style = 33 }
assert_raise(ArgumentError) { @format_o.format_style = 33 }
end
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
old_verbose, $VERBOSE = $VERBOSE, nil
Racc_Runtime_Version = '1.4.5'
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_Type = 'ruby'
end
$VERBOSE = old_verbose
def Parser.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 VERSION #:nodoc:
MAJOR = 2
MINOR = 2
TINY = 2
MINOR = 3
TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.')
end

View file

@ -1 +1,2 @@
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'
$:.unshift "#{File.dirname(__FILE__)}/../lib"
@ -9,8 +10,15 @@ require 'action_mailer/test_case'
# Show backtraces for deprecated behavior for quicker cleanup.
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"
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
def self.deliveries
@ -37,7 +45,6 @@ class Net::SMTP
end
def uses_gem(gem_name, test_name, version = '> 0')
require 'rubygems'
gem gem_name.to_s, version
require gem_name.to_s
yield
@ -45,13 +52,6 @@ rescue LoadError
$stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again."
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)
@old_delivery_method = ActionMailer::Base.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
def example_format(text)
"<em><strong><small>#{text}</small></strong></em>"
"<em><strong><small>#{h(text)}</small></strong></em>".html_safe!
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"
body render(:inline => "Hello, <%= @world %>", :layout => false, :body => { :world => "Earth" })
end
def multipart(recipient, type = nil)
recipients recipient
subject "You have a mail"
from "tester@example.com"
content_type(type) if type
end
end
class ExplicitLayoutMailer < ActionMailer::Base
@ -56,6 +64,43 @@ class LayoutMailerTest < Test::Unit::TestCase
assert_equal "Hello from layout Inside", mail.body.strip
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
mail = AutoLayoutMailer.create_spam(@recipient)
assert_equal "Spammer layout Hello, Earth", mail.body.strip

View file

@ -18,7 +18,6 @@ class TestMailer < ActionMailer::Base
@recipients = recipient
@subject = "[Signed up] Welcome #{recipient}"
@from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient
end
@ -289,8 +288,6 @@ class TestMailer < ActionMailer::Base
end
end
uses_mocha 'ActionMailerTest' do
class ActionMailerTest < Test::Unit::TestCase
include ActionMailer::Quoting
@ -332,6 +329,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type
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/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type
@ -357,12 +355,14 @@ class ActionMailerTest < Test::Unit::TestCase
end
def test_signed_up
Time.stubs(:now => Time.now)
expected = new_mail
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
expected.date = Time.now
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
@ -389,6 +389,8 @@ class ActionMailerTest < Test::Unit::TestCase
end
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
expected = new_mail
expected.to = @recipient
@ -568,7 +570,9 @@ class ActionMailerTest < Test::Unit::TestCase
mail = TestMailer.create_signed_up(@recipient)
logger = mock()
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.deliver_signed_up(@recipient)
end
@ -799,6 +803,8 @@ EOF
end
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)
assert_equal 3, mail.parts.length
assert_equal "1.0", mail.mime_version
@ -812,6 +818,8 @@ EOF
end
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"])
assert_equal 3, mail.parts.length
assert_equal "text/html", mail.parts[0].content_type
@ -915,6 +923,8 @@ EOF
def test_multipart_with_template_path_with_dots
mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient)
assert_equal 2, mail.parts.length
assert_equal 'text/plain', mail.parts[0].content_type
assert_equal 'utf-8', mail.parts[0].charset
end
def test_custom_content_type_attributes
@ -932,6 +942,7 @@ EOF
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_return_path
assert_match %r{^Return-Path: <another@somewhere.test>}, MockSMTP.deliveries[0][0]
assert_equal "another@somewhere.test", MockSMTP.deliveries[0][1].to_s
end
def test_body_is_stored_as_an_ivar
@ -940,6 +951,7 @@ EOF
end
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(:enable_starttls_auto)
ActionMailer::Base.delivery_method = :smtp
@ -947,25 +959,34 @@ EOF
end
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(:enable_starttls_auto).never
ActionMailer::Base.delivery_method = :smtp
TestMailer.deliver_signed_up(@recipient)
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
def test_attr
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.template_root = 'test/path'
assert_equal 'test/path', sub.template_root
assert_equal expected, FunkyPathMailer.template_root
assert_equal 'test/path', sub.template_root.to_s
assert_equal expected, FunkyPathMailer.template_root.to_s
end
end
@ -1051,7 +1072,7 @@ class RespondToTest < Test::Unit::TestCase
end
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
end

View file

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

View file

@ -26,7 +26,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_determine_default_mailer_raises_correct_error
assert_raises(ActionMailer::NonInferrableMailerError) do
assert_raise(ActionMailer::NonInferrableMailerError) do
self.class.determine_default_mailer("NotAMailerTest")
end
end
@ -36,7 +36,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_encode
assert_equal "=?utf-8?Q?=0aasdf=0a?=", encode("\nasdf\n")
assert_equal "=?utf-8?Q?=0Aasdf=0A?=", encode("\nasdf\n")
end
def test_assert_emails
@ -84,7 +84,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
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
TestHelperMailer.deliver_test
end
@ -94,7 +94,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
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
TestHelperMailer.deliver_test
TestHelperMailer.deliver_test
@ -105,7 +105,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
def test_assert_no_emails_failure
error = assert_raises Test::Unit::AssertionFailedError do
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do
TestHelperMailer.deliver_test
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
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
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
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
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
and can therefore share helper methods
BlogController < ActionController::Base
CustomersController < ActionController::Base
def show
@customer = find_customer
end
@ -42,7 +42,7 @@ A short rundown of the major features:
@customer = find_customer
@customer.attributes = params[:customer]
@customer.save ?
redirect_to(:action => "display") :
redirect_to(:action => "show") :
render(:action => "edit")
end
@ -59,7 +59,7 @@ A short rundown of the major features:
Title: <%= post.title %>
<% end %>
All post titles: <%= @post.collect{ |p| p.title }.join ", " %>
All post titles: <%= @posts.collect{ |p| p.title }.join ", " %>
<% unless @person.is_client? %>
Not for clients to see...
@ -123,7 +123,7 @@ A short rundown of the major features:
<%= text_field "post", "title", "size" => 30 %>
<%= html_date_select(Date.today) %>
<%= link_to "New post", :controller => "post", :action => "new" %>
<%= truncate(post.title, 25) %>
<%= truncate(post.title, :length => 25) %>
{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]
* 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
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
as well:
Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
Params: {"controller"=>"weblog", "action"=>"create",
Processing PostsController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
Params: {"controller"=>"posts", "action"=>"create",
"post"=>{"title"=>"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%)
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]
* 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
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
def create
post = Post.create(params[:post])
redirect_to :action => "display", :id => post.id
redirect_to :action => "show", :id => post.id
end
end
@ -362,7 +323,7 @@ methods:
@posts = Post.find(:all)
end
def display
def show
@post = Post.find(params[:id])
end
@ -372,7 +333,7 @@ methods:
def create
@post = Post.create(params[:post])
redirect_to :action => "display", :id => @post.id
redirect_to :action => "show", :id => @post.id
end
end
@ -385,48 +346,33 @@ request from the web-server (like to be Apache).
And the templates look like this:
weblog/layout.erb:
weblog/layout.html.erb:
<html><body>
<%= yield %>
</body></html>
weblog/index.erb:
weblog/index.html.erb:
<% 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 %>
weblog/display.erb:
weblog/show.html.erb:
<p>
<b><%= post.title %></b><br/>
<b><%= post.content %></b>
<b><%= @post.title %></b><br/>
<b><%= @post.content %></b>
</p>
weblog/new.erb:
weblog/new.html.erb:
<%= form "post" %>
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
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
new model). After creating the post, it'll redirect to the display page using
an URL such as /weblog/display/5 (where 5 is the id of the post).
new model). After creating the post, it'll redirect to the show page using
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
The latest version of Action Pack can be found at

View file

@ -4,7 +4,6 @@ require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version')
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
# 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.warning = true
@ -80,7 +79,8 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true
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.autorequire = 'action_controller'
@ -136,12 +136,14 @@ task :update_js => [ :update_scriptaculous ]
desc "Publish the API documentation"
task :pgem => [:package] do
require 'rake/contrib/sshpublisher'
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'`
end
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
require 'rake/contrib/sshpublisher'
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload
end

View file

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

View file

@ -1,69 +0,0 @@
require 'test/unit/assertions'
module ActionController #:nodoc:
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
#
# For cookies, you need to manually create the cookie, like this:
#
# @request.cookies["key"] = CGI::Cookie.new("key", "value")
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
module Assertions
def self.included(klass)
%w(response selector tag dom routing model).each do |kind|
require "action_controller/assertions/#{kind}_assertions"
klass.module_eval { include const_get("#{kind.camelize}Assertions") }
end
end
def clean_backtrace(&block)
yield
rescue Test::Unit::AssertionFailedError => error
framework_path = Regexp.new(File.expand_path("#{File.dirname(__FILE__)}/assertions"))
error.backtrace.reject! { |line| File.expand_path(line) =~ framework_path }
raise
end
end
end
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
include ActionController::Assertions
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,13 +2,6 @@ require 'fileutils'
require 'uri'
require 'set'
require 'action_controller/caching/pages'
require 'action_controller/caching/actions'
require 'action_controller/caching/sql_cache'
require 'action_controller/caching/sweeping'
require 'action_controller/caching/fragments'
module ActionController #:nodoc:
# Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
# around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment.
@ -29,8 +22,15 @@ module ActionController #:nodoc:
# ActionController::Base.cache_store = :file_store, "/path/to/cache/directory"
# ActionController::Base.cache_store = :drb_store, "druby://localhost:9192"
# ActionController::Base.cache_store = :mem_cache_store, "localhost"
# ActionController::Base.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211")
# ActionController::Base.cache_store = MyOwnStore.new("parameter")
module Caching
autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages'
autoload :Sweeper, 'action_controller/caching/sweeper'
autoload :Sweeping, 'action_controller/caching/sweeping'
def self.included(base) #:nodoc:
base.class_eval do
@@cache_store = nil
@ -42,7 +42,7 @@ module ActionController #:nodoc:
end
include Pages, Actions, Fragments
include Sweeping, SqlCache if defined?(ActiveRecord)
include Sweeping if defined?(ActiveRecord)
@@perform_caching = true
cattr_accessor :perform_caching
@ -63,7 +63,6 @@ module ActionController #:nodoc:
end
end
private
def 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) }
cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options)
around_filter(cache_filter, filter_options)
around_filter(filter_options) do |controller, action|
cache_filter.filter(controller, action)
end
end
end
@ -83,6 +85,12 @@ module ActionController #:nodoc:
@options = options
end
def filter(controller, action)
should_continue = before(controller)
action.call if should_continue
after(controller)
end
def before(controller)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
if cache = controller.read_fragment(cache_path.path, @options[:store_options])
@ -113,7 +121,7 @@ module ActionController #:nodoc:
end
def caching_allowed(controller)
controller.request.get? && controller.response.headers['Status'].to_i == 200
controller.request.get? && controller.response.status.to_i == 200
end
def cache_layout?
@ -129,24 +137,23 @@ module ActionController #:nodoc:
attr_reader :path, :extension
class << self
def path_for(controller, options, infer_extension=true)
def path_for(controller, options, infer_extension = true)
new(controller, options, infer_extension).path
end
end
# When true, infer_extension will look up the cache path extension from the request's path & format.
# This is desirable when reading and writing the cache, but not when expiring the cache - expire_action should expire the same files regardless of the request format.
def initialize(controller, options = {}, infer_extension=true)
if infer_extension and options.is_a? Hash
request_extension = extract_extension(controller.request)
options = options.reverse_merge(:format => request_extension)
# This is desirable when reading and writing the cache, but not when expiring the cache -
# expire_action should expire the same files regardless of the request format.
def initialize(controller, options = {}, infer_extension = true)
if infer_extension
extract_extension(controller.request)
options = options.reverse_merge(:format => @extension) if options.is_a?(Hash)
end
path = controller.url_for(options).split('://').last
normalize!(path)
if infer_extension
@extension = request_extension
add_extension!(path, @extension)
end
add_extension!(path, @extension)
@path = URI.unescape(path)
end
@ -162,13 +169,7 @@ module ActionController #:nodoc:
def extract_extension(request)
# Don't want just what comes after the last '.' to accommodate multi part extensions
# such as tar.gz.
extension = request.path[/^[^.]+\.(.+)$/, 1]
# If there's no extension in the path, check request.format
if extension.nil?
extension = request.cache_format
end
extension
@extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format
end
end
end

View file

@ -50,7 +50,7 @@ module ActionController #:nodoc:
# Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
def write_fragment(key, content, options = nil)
return unless cache_configured?
return content unless cache_configured?
key = fragment_cache_key(key)
@ -83,15 +83,23 @@ module ActionController #:nodoc:
end
end
# Name can take one of three forms:
# * String: This would normally take the form of a path like "pages/45/notes"
# * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
# * Regexp: Will destroy all the matched fragments, example:
# %r{pages/\d*/notes}
# Ensure you do not specify start and finish in the regex (^$) because
# the actual filename matched looks like ./cache/filename/path.cache
# Regexp expiration is only supported on caches that can iterate over
# all keys (unlike memcached).
# Removes fragments from the cache.
#
# +key+ can take one of three forms:
# * String - This would normally take the form of a path, like
# <tt>"pages/45/notes"</tt>.
# * Hash - Treated as an implicit call to +url_for+, like
# <tt>{:controller => "pages", :action => "notes", :id => 45}</tt>
# * Regexp - Will remove any fragment that matches, so
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
# the actual filename matched looks like
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
# only supported on caches that can iterate over all keys (unlike
# memcached).
#
# +options+ is passed through to the cache store's <tt>delete</tt>
# method (or <tt>delete_matched</tt>, for Regexp keys.)
def expire_fragment(key, options = nil)
return unless cache_configured?

View file

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

View file

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

View file

@ -0,0 +1,45 @@
require 'active_record'
module ActionController #:nodoc:
module Caching
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end

View file

@ -51,47 +51,5 @@ module ActionController #:nodoc:
end
end
end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments)
return if @controller.nil?
@controller.__send__(method, *arguments)
end
end
end
end
end

View file

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

View file

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

View file

@ -1,53 +0,0 @@
require 'digest/md5'
require 'cgi/session'
require 'cgi/session/pstore'
class CGI #:nodoc:
# * Expose the CGI instance to session stores.
# * Don't require 'digest/md5' whenever a new session id is generated.
class Session #:nodoc:
def self.generate_unique_id(constant = nil)
ActiveSupport::SecureRandom.hex(16)
end
# Make the CGI instance available to session stores.
attr_reader :cgi
attr_reader :dbman
alias_method :initialize_without_cgi_reader, :initialize
def initialize(cgi, options = {})
@cgi = cgi
initialize_without_cgi_reader(cgi, options)
end
private
# Create a new session id.
def create_new_id
@new_session = true
self.class.generate_unique_id
end
# * Don't require 'digest/md5' whenever a new session is started.
class PStore #:nodoc:
def initialize(session, option={})
dir = option['tmpdir'] || Dir::tmpdir
prefix = option['prefix'] || ''
id = session.session_id
md5 = Digest::MD5.hexdigest(id)[0,16]
path = dir+"/"+prefix+md5
path.untaint
if File::exist?(path)
@hash = nil
else
unless session.new_session
raise CGI::Session::NoSession, "uninitialized session"
end
@hash = {}
end
@p = ::PStore.new(path)
@p.transaction do |p|
File.chmod(0600, p.path)
end
end
end
end
end

View file

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

View file

@ -1,169 +0,0 @@
module ActionController #:nodoc:
# Components allow you to call other actions for their rendered response while executing another action. You can either delegate
# the entire response rendering or you can mix a partial response in with your other content.
#
# class WeblogController < ActionController::Base
# # Performs a method and then lets hello_world output its render
# def delegate_action
# do_other_stuff_before_hello_world
# render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
# end
# end
#
# class GreeterController < ActionController::Base
# def hello_world
# render :text => "#{params[:person]} says, Hello World!"
# end
# end
#
# The same can be done in a view to do a partial rendering:
#
# Let's see a greeting:
# <%= render_component :controller => "greeter", :action => "hello_world" %>
#
# It is also possible to specify the controller as a class constant, bypassing the inflector
# code to compute the controller class at runtime:
#
# <%= render_component :controller => GreeterController, :action => "hello_world" %>
#
# == When to use components
#
# Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
# conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
# reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
# across many applications at once.
#
# So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
module Components
def self.included(base) #:nodoc:
base.class_eval do
include InstanceMethods
include ActiveSupport::Deprecation
extend ClassMethods
helper HelperMethods
# If this controller was instantiated to process a component request,
# +parent_controller+ points to the instantiator of this controller.
attr_accessor :parent_controller
alias_method_chain :process_cleanup, :components
alias_method_chain :set_session_options, :components
alias_method_chain :flash, :components
alias_method :component_request?, :parent_controller
end
end
module ClassMethods
# Track parent controller to identify component requests
def process_with_components(request, response, parent_controller = nil) #:nodoc:
controller = new
controller.parent_controller = parent_controller
controller.process(request, response)
end
end
module HelperMethods
def render_component(options)
@controller.__send__(:render_component_as_string, options)
end
end
module InstanceMethods
# Extracts the action_name from the request parameters and performs that action.
def process_with_components(request, response, method = :perform_action, *arguments) #:nodoc:
flash.discard if component_request?
process_without_components(request, response, method, *arguments)
end
protected
# Renders the component specified as the response for the current method
def render_component(options) #:doc:
component_logging(options) do
render_for_text(component_response(options, true).body, response.headers["Status"])
end
end
deprecate :render_component => "Please install render_component plugin from http://github.com/rails/render_component/tree/master"
# Returns the component response as a string
def render_component_as_string(options) #:doc:
component_logging(options) do
response = component_response(options, false)
if redirected = response.redirected_to
render_component_as_string(redirected)
else
response.body
end
end
end
deprecate :render_component_as_string => "Please install render_component plugin from http://github.com/rails/render_component/tree/master"
def flash_with_components(refresh = false) #:nodoc:
if !defined?(@_flash) || refresh
@_flash =
if defined?(@parent_controller)
@parent_controller.flash
else
flash_without_components
end
end
@_flash
end
private
def component_response(options, reuse_response)
klass = component_class(options)
request = request_for_component(klass.controller_name, options)
new_response = reuse_response ? response : response.dup
klass.process_with_components(request, new_response, self)
end
# determine the controller class for the component request
def component_class(options)
if controller = options[:controller]
controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
else
self.class
end
end
# Create a new request object based on the current request.
# The new request inherits the session from the current request,
# bypassing any session options set for the component controller's class
def request_for_component(controller_name, options)
new_request = request.dup
new_request.session = request.session
new_request.instance_variable_set(
:@parameters,
(options[:params] || {}).with_indifferent_access.update(
"controller" => controller_name, "action" => options[:action], "id" => options[:id]
)
)
new_request
end
def component_logging(options)
if logger
logger.info "Start rendering component (#{options.inspect}): "
result = yield
logger.info "\n\nEnd of component rendering"
result
else
yield
end
end
def set_session_options_with_components(request)
set_session_options_without_components(request) unless component_request?
end
def process_cleanup_with_components
process_cleanup_without_components unless component_request?
end
end
end
end

View file

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

View file

@ -2,27 +2,16 @@ module ActionController
# Dispatches requests to the appropriate controller and takes care of
# reloading the app after each request when Dependencies.load? is true.
class Dispatcher
@@guard = Mutex.new
@@cache_classes = true
class << self
def define_dispatcher_callbacks(cache_classes)
@@cache_classes = cache_classes
unless cache_classes
# Development mode callbacks
before_dispatch :reload_application
after_dispatch :cleanup_application
end
# Common callbacks
to_prepare :load_application_controller do
begin
require_dependency 'application' unless defined?(::ApplicationController)
rescue LoadError => error
raise unless error.message =~ /application\.rb/
end
ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
end
if defined?(ActiveRecord)
after_dispatch :checkin_connections
to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
end
@ -33,8 +22,7 @@ module ActionController
end
end
# Backward-compatible class method takes CGI-specific args. Deprecated
# in favor of Dispatcher.new(output, request, response).dispatch.
# DEPRECATE: Remove CGI support
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
new(output).dispatch_cgi(cgi, session_options)
end
@ -53,144 +41,93 @@ module ActionController
@prepare_dispatch_callbacks.replace_or_append!(callback)
end
# If the block raises, send status code as a last-ditch response.
def failsafe_response(fallback_output, status, originating_exception = nil)
yield
rescue Exception => exception
begin
log_failsafe_exception(status, originating_exception || exception)
body = failsafe_response_body(status)
fallback_output.write "Status: #{status}\r\nContent-Type: text/html\r\n\r\n#{body}"
nil
rescue Exception => failsafe_error # Logger or IO errors
$stderr.puts "Error during failsafe response: #{failsafe_error}"
$stderr.puts "(originally #{originating_exception})" if originating_exception
def run_prepare_callbacks
if defined?(Rails) && Rails.logger
logger = Rails.logger
else
logger = Logger.new($stderr)
end
new(logger).send :run_callbacks, :prepare_dispatch
end
private
def failsafe_response_body(status)
error_path = "#{error_file_path}/#{status.to_s[0..3]}.html"
def reload_application
# Run prepare callbacks before every request in development mode
run_prepare_callbacks
if File.exist?(error_path)
File.read(error_path)
else
"<html><body><h1>#{status}</h1></body></html>"
end
end
Routing::Routes.reload
end
def log_failsafe_exception(status, exception)
message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: #{status}\n"
message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
failsafe_logger.fatal message
end
def failsafe_logger
if defined?(::RAILS_DEFAULT_LOGGER) && !::RAILS_DEFAULT_LOGGER.nil?
::RAILS_DEFAULT_LOGGER
else
Logger.new($stderr)
end
end
def cleanup_application
# Cleanup the application before processing the current request.
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end
end
cattr_accessor :error_file_path
self.error_file_path = Rails.public_path if defined?(Rails.public_path)
cattr_accessor :middleware
self.middleware = MiddlewareStack.new do |middleware|
middlewares = File.join(File.dirname(__FILE__), "middlewares.rb")
middleware.instance_eval(File.read(middlewares))
end
include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
# DEPRECATE: Remove arguments, since they are only used by CGI
def initialize(output = $stdout, request = nil, response = nil)
@output, @request, @response = output, request, response
@output = output
build_middleware_stack if @@cache_classes
end
def dispatch_unlocked
def dispatch
begin
run_callbacks :before_dispatch
handle_request
Routing::Routes.call(@env)
rescue Exception => exception
failsafe_rescue exception
if controller ||= (::ApplicationController rescue Base)
controller.call_with_exception(@env, exception).to_a
else
raise exception
end
ensure
run_callbacks :after_dispatch, :enumerator => :reverse_each
end
end
def dispatch
if ActionController::Base.allow_concurrency
dispatch_unlocked
# DEPRECATE: Remove CGI support
def dispatch_cgi(cgi, session_options)
CGIHandler.dispatch_cgi(self, cgi, @output)
end
def call(env)
if @@cache_classes
@app.call(env)
else
@@guard.synchronize do
dispatch_unlocked
Reloader.run do
# When class reloading is turned on, we will want to rebuild the
# middleware stack every time we process a request. If we don't
# rebuild the middleware stack, then the stack may contain references
# to old classes metal classes, which will b0rk class reloading.
build_middleware_stack
@app.call(env)
end
end
end
def dispatch_cgi(cgi, session_options)
if cgi ||= self.class.failsafe_response(@output, '400 Bad Request') { CGI.new }
@request = CgiRequest.new(cgi, session_options)
@response = CgiResponse.new(cgi)
dispatch
end
rescue Exception => exception
failsafe_rescue exception
end
def call(env)
@request = RackRequest.new(env)
@response = RackResponse.new(@request)
def _call(env)
@env = env
dispatch
end
def reload_application
# Run prepare callbacks before every request in development mode
run_callbacks :prepare_dispatch
Routing::Routes.reload
ActionController::Base.view_paths.reload!
ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear
end
# Cleanup the application by clearing out loaded classes so they can
# be reloaded on the next request without restarting the server.
def cleanup_application
ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
ActiveSupport::Dependencies.clear
ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
end
def flush_logger
Base.logger.flush
end
def mark_as_test_request!
@test_request = true
self
end
def test_request?
@test_request
end
def checkin_connections
# Don't return connection (and peform implicit rollback) if this request is a part of integration test
return if test_request?
ActiveRecord::Base.clear_active_connections!
end
protected
def handle_request
@controller = Routing::Routes.recognize(@request)
@controller.process(@request, @response).out(@output)
end
def failsafe_rescue(exception)
self.class.failsafe_response(@output, '500 Internal Server Error', exception) do
if @controller ||= defined?(::ApplicationController) ? ::ApplicationController : Base
@controller.process_with_exception(@request, @response, exception).out(@output)
else
raise exception
end
end
private
def build_middleware_stack
@app = @@middleware.build(lambda { |env| self.dup._call(env) })
end
end
end

View file

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

View file

@ -4,20 +4,22 @@ module ActionController #:nodoc:
# action that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done. Example:
#
# class WeblogController < ActionController::Base
# class PostsController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Successfully created post"
# redirect_to :action => "display", :params => { :id => post.id }
# redirect_to posts_path(@post)
# end
#
# def display
# def show
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# display.erb
# <% if flash[:notice] %><div class="notice"><%= flash[:notice] %></div><% end %>
# show.html.erb
# <% if flash[:notice] %>
# <div class="notice"><%= flash[:notice] %></div>
# <% end %>
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as
# many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
@ -27,12 +29,11 @@ module ActionController #:nodoc:
def self.included(base)
base.class_eval do
include InstanceMethods
alias_method_chain :assign_shortcuts, :flash
alias_method_chain :reset_session, :flash
alias_method_chain :perform_action, :flash
alias_method_chain :reset_session, :flash
end
end
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
@ -119,6 +120,11 @@ module ActionController #:nodoc:
(@used.keys - keys).each{ |k| @used.delete(k) }
end
def store(session, key = "flash")
return if self.empty?
session[key] = self
end
private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
@ -136,37 +142,30 @@ module ActionController #:nodoc:
module InstanceMethods #:nodoc:
protected
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash)
flash(:refresh)
def perform_action_with_flash
perform_action_without_flash
if defined? @_flash
@_flash.store(session)
remove_instance_variable(:@_flash)
end
end
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
# <tt>flash["notice"] = "hello"</tt> to put a new one.
# Note that if sessions are disabled only flash.now will work.
def flash(refresh = false) #:doc:
if !defined?(@_flash) || refresh
@_flash =
if session.is_a?(Hash)
# don't put flash in session if disabled
FlashHash.new
else
# otherwise, session is a CGI::Session or a TestSession
# so make sure it gets retrieved from/saved to session storage after request processing
session["flash"] ||= FlashHash.new
end
def reset_session_with_flash
reset_session_without_flash
remove_instance_variable(:@_flash) if defined? @_flash
end
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash #:doc:
if !defined?(@_flash)
@_flash = session["flash"] || FlashHash.new
@_flash.sweep
end
@_flash
end
private
def assign_shortcuts_with_flash(request, response) #:nodoc:
assign_shortcuts_without_flash(request, response)
flash(:refresh)
flash.sweep if @_session && !component_request?
end
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,6 @@ module ActionController #:nodoc:
module RequestForgeryProtection
def self.included(base)
base.class_eval do
class_inheritable_accessor :request_forgery_protection_options
self.request_forgery_protection_options = {}
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@ -14,7 +12,7 @@ module ActionController #:nodoc:
end
# Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current web application, not a
# forged link from another site, is done by embedding a token based on the session (which an attacker wouldn't know) in all
# forged link from another site, is done by embedding a token based on a random string stored in the session (which an attacker wouldn't know) in all
# forms and Ajax requests generated by Rails and then verifying the authenticity of that token in the controller. Only
# HTML/JavaScript requests are checked, so this will not protect your XML API (presumably you'll have a different authentication
# scheme there anyway). Also, GET requests are not protected as these should be idempotent anyway.
@ -57,12 +55,8 @@ module ActionController #:nodoc:
# Example:
#
# class FooController < ApplicationController
# # uses the cookie session store (then you don't need a separate :secret)
# protect_from_forgery :except => :index
#
# # uses one of the other session stores that uses a session_id value.
# protect_from_forgery :secret => 'my-little-pony', :except => :index
#
# # you can disable csrf protection on controller-by-controller basis:
# skip_before_filter :verify_authenticity_token
# end
@ -70,13 +64,12 @@ module ActionController #:nodoc:
# Valid Options:
#
# * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified.
# * <tt>:secret</tt> - Custom salt used to generate the <tt>form_authenticity_token</tt>.
# Leave this off if you are using the cookie session store.
# * <tt>:digest</tt> - Message digest used for hashing. Defaults to 'SHA1'.
def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token
before_filter :verify_authenticity_token, :only => options.delete(:only), :except => options.delete(:except)
request_forgery_protection_options.update(options)
if options[:secret] || options[:digest]
ActiveSupport::Deprecation.warn("protect_from_forgery only takes :only and :except options now. :digest and :secret have no effect", caller)
end
end
end
@ -88,14 +81,19 @@ module ActionController #:nodoc:
# Returns true or false if a request is verified. Checks:
#
# * is the format restricted? By default, only HTML and AJAX requests are checked.
# * is the format restricted? By default, only HTML requests are checked.
# * is it a GET request? Gets should be safe and idempotent
# * Does the form_authenticity_token match the given _token value from the params?
# * Does the form_authenticity_token match the given token value from the params?
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
request.xhr? ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
form_authenticity_token == form_authenticity_param
end
def form_authenticity_param
params[request_forgery_protection_token]
end
def verifiable_request_format?
@ -105,32 +103,7 @@ module ActionController #:nodoc:
# Sets the token value for the current session. Pass a <tt>:secret</tt> option
# in +protect_from_forgery+ to add a custom salt to the hash.
def form_authenticity_token
@form_authenticity_token ||= if !session.respond_to?(:session_id)
raise InvalidAuthenticityToken, "Request Forgery Protection requires a valid session. Use #allow_forgery_protection to disable it, or use a valid session."
elsif request_forgery_protection_options[:secret]
authenticity_token_from_session_id
elsif session.respond_to?(:dbman) && session.dbman.respond_to?(:generate_digest)
authenticity_token_from_cookie_session
else
raise InvalidAuthenticityToken, "No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store)."
end
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])
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
end
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:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
# is already implemented by the Action Controller, but the public view should be tailored to your specific application.
# Actions that fail to perform as expected throw exceptions. These
# exceptions can either be rescued for the public view (with a nice
# user-friendly explanation) or for the developers view (with tons of
# debugging information). The developers view is already implemented by
# the Action Controller, but the public view should be tailored to your
# specific application.
#
# The default behavior for public exceptions is to render a static html file with the name of the error code thrown. If no such
# file exists, an empty response is sent with the correct status code.
# The default behavior for public exceptions is to render a static html
# file with the name of the error code thrown. If no such file exists, an
# empty response is sent with the correct status code.
#
# You can override what constitutes a local request by overriding the <tt>local_request?</tt> method in your own controller.
# Custom rescue behavior is achieved by overriding the <tt>rescue_action_in_public</tt> and <tt>rescue_action_locally</tt> methods.
# You can override what constitutes a local request by overriding the
# <tt>local_request?</tt> method in your own controller. Custom rescue
# behavior is achieved by overriding the <tt>rescue_action_in_public</tt>
# and <tt>rescue_action_locally</tt> methods.
module Rescue
LOCALHOST = '127.0.0.1'.freeze
@ -32,6 +38,9 @@ module ActionController #:nodoc:
'ActionView::TemplateError' => 'template_error'
}
RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new_and_loaded(
File.join(File.dirname(__FILE__), "templates"))
def self.included(base) #:nodoc:
base.cattr_accessor :rescue_responses
base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
@ -50,47 +59,60 @@ module ActionController #:nodoc:
end
module ClassMethods
def process_with_exception(request, response, exception) #:nodoc:
def call_with_exception(env, exception) #:nodoc:
request = env["action_controller.rescue.request"] ||= Request.new(env)
response = env["action_controller.rescue.response"] ||= Response.new
new.process(request, response, :rescue_action, exception)
end
end
protected
# Exception handler called when the performance of an action raises an exception.
# Exception handler called when the performance of an action raises
# an exception.
def rescue_action(exception)
rescue_with_handler(exception) || rescue_action_without_handler(exception)
rescue_with_handler(exception) ||
rescue_action_without_handler(exception)
end
# Overwrite to implement custom logging of errors. By default logs as fatal.
# Overwrite to implement custom logging of errors. By default
# logs as fatal.
def log_error(exception) #:doc:
ActiveSupport::Deprecation.silence do
if ActionView::TemplateError === exception
logger.fatal(exception.to_s)
else
logger.fatal(
"\n\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") +
"\n\n"
"\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") + "\n\n"
)
end
end
end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>). By
# default will call render_optional_error_file. Override this method to provide more user friendly error messages.
# Overwrite to implement public exception handling (for requests
# answering false to <tt>local_request?</tt>). By default will call
# render_optional_error_file. Override this method to provide more
# user friendly error messages.
def rescue_action_in_public(exception) #:doc:
render_optional_error_file response_code_for_rescue(exception)
end
# Attempts to render a static error page based on the <tt>status_code</tt> thrown,
# or just return headers if no such file exists. For example, if a 500 error is
# being handled Rails will first attempt to render the file at <tt>public/500.html</tt>.
# If the file doesn't exist, the body of the response will be left empty.
# Attempts to render a static error page based on the
# <tt>status_code</tt> thrown, or just return headers if no such file
# exists. At first, it will try to render a localized static page.
# For example, if a 500 error is being handled Rails and locale is :da,
# it will first attempt to render the file at <tt>public/500.da.html</tt>
# then attempt to render <tt>public/500.html</tt>. If none of them exist,
# the body of the response will be left empty.
def render_optional_error_file(status_code)
status = interpret_status(status_code)
locale_path = "#{Rails.public_path}/#{status[0,3]}.#{I18n.locale}.html" if I18n.locale
path = "#{Rails.public_path}/#{status[0,3]}.html"
if File.exist?(path)
render :file => path, :status => status
if locale_path && File.exist?(locale_path)
render :file => locale_path, :status => status, :content_type => Mime::HTML
elsif File.exist?(path)
render :file => path, :status => status, :content_type => Mime::HTML
else
head status
end
@ -107,11 +129,13 @@ module ActionController #:nodoc:
# a controller action.
def rescue_action_locally(exception)
@template.instance_variable_set("@exception", exception)
@template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
@template.instance_variable_set("@contents", @template.render(:file => template_path_for_local_rescue(exception)))
@template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH)
@template.instance_variable_set("@contents",
@template.render(:file => template_path_for_local_rescue(exception)))
response.content_type = Mime::HTML
render_for_file(rescues_path("layout"), response_code_for_rescue(exception))
render_for_file(rescues_path("layout"),
response_code_for_rescue(exception))
end
def rescue_action_without_handler(exception)
@ -139,7 +163,7 @@ module ActionController #:nodoc:
end
def rescues_path(template_name)
"#{File.dirname(__FILE__)}/templates/rescues/#{template_name}.erb"
RESCUES_TEMPLATE_PATH["rescues/#{template_name}.erb"]
end
def template_path_for_local_rescue(exception)
@ -151,13 +175,9 @@ module ActionController #:nodoc:
end
def clean_backtrace(exception)
if backtrace = exception.backtrace
if defined?(RAILS_ROOT)
backtrace.map { |line| line.sub RAILS_ROOT, '' }
else
backtrace
end
end
defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
Rails.backtrace_cleaner.clean(exception.backtrace) :
exception.backtrace
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

@ -65,7 +65,7 @@ module ActionController
# rather than triggering the expensive logic in +url_for+.
class PositionalArguments < Optimiser
def guard_conditions
number_of_arguments = route.segment_keys.size
number_of_arguments = route.required_segment_keys.size
# if they're using foo_url(:id=>2) it's one
# argument, but we don't want to generate /foos/id2
if number_of_arguments == 1

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,167 +1,221 @@
require 'cgi'
require 'cgi/session'
require 'openssl' # to generate the HMAC message digest
module ActionController
module Session
# This cookie-based session store is the Rails default. Sessions typically
# contain at most a user_id and flash message; both fit within the 4K cookie
# size limit. Cookie-based sessions are dramatically faster than the
# alternatives.
#
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his +user_id+ without knowing the secret key
# included in the hash. New apps are generated with a pregenerated secret
# in config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
#
# * <tt>:secret</tt>: An application-wide key string or block returning a
# string called per generated digest. The block is called with the
# CGI::Session instance as an argument. It's important that the secret
# is not vulnerable to a dictionary attack. Therefore, you should choose
# a secret consisting of random numbers and letters and more than 30
# characters. Examples:
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: The message digest algorithm used to verify session
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# "rake secret" and set the key in config/environment.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CookieStore
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
# This cookie-based session store is the Rails default. Sessions typically
# contain at most a user_id and flash message; both fit within the 4K cookie
# size limit. Cookie-based sessions are dramatically faster than the
# alternatives.
#
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
# TamperedWithCookie is raised if the data integrity check fails.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his +user_id+ without knowing the secret key included in
# the hash. New apps are generated with a pregenerated secret in
# config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
#
# * <tt>:secret</tt>: An application-wide key string or block returning a string
# called per generated digest. The block is called with the CGI::Session
# instance as an argument. It's important that the secret is not vulnerable to
# a dictionary attack. Therefore, you should choose a secret consisting of
# random numbers and letters and more than 30 characters. Examples:
#
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
# :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: The message digest algorithm used to verify session
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# "rake secret" and set the key in config/environment.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CGI::Session::CookieStore
# Cookies can typically store 4096 bytes.
MAX = 4096
SECRET_MIN_LENGTH = 30 # characters
DEFAULT_OPTIONS = {
:key => '_session_id',
:domain => nil,
:path => "/",
:expire_after => nil,
:httponly => true
}.freeze
# Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end
ENV_SESSION_KEY = "rack.session".freeze
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
HTTP_SET_COOKIE = "Set-Cookie".freeze
# Raised when the cookie fails its integrity check.
class TamperedWithCookie < StandardError; end
# Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end
# Called from CGI::Session only.
def initialize(session, options = {})
# The session_key option is required.
if options['session_key'].blank?
raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
end
# The secret option is required.
ensure_secret_secure(options['secret'])
# Keep the session and its secret on hand so we can read and write cookies.
@session, @secret = session, options['secret']
# Message digest defaults to SHA1.
@digest = options['digest'] || 'SHA1'
# Default cookie options derived from session settings.
@cookie_options = {
'name' => options['session_key'],
'path' => options['session_path'],
'domain' => options['session_domain'],
'expires' => options['session_expires'],
'secure' => options['session_secure'],
'http_only' => options['session_http_only']
}
# Set no_hidden and no_cookies since the session id is unused and we
# set our own data cookie.
options['no_hidden'] = true
options['no_cookies'] = true
end
# To prevent users from using something insecure like "Password" we make sure that the
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
# There's no way we can do this check if they've provided a proc for the
# secret.
return true if secret.is_a?(Proc)
if secret.blank?
raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
end
if secret.length < SECRET_MIN_LENGTH
raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
end
end
# Restore session data from the cookie.
def restore
@original = read_cookie
@data = unmarshal(@original) || {}
end
# Wait until close to write the session data cookie.
def update; end
# Write the session data cookie if it was loaded and has changed.
def close
if defined?(@data) && !@data.blank?
updated = marshal(@data)
raise CookieOverflow if updated.size > MAX
write_cookie('value' => updated) unless updated == @original
end
end
# Delete the session data by setting an expired cookie with no data.
def delete
@data = nil
clear_old_cookie_value
write_cookie('value' => nil, 'expires' => 1.year.ago)
end
# Generate the HMAC keyed message digest. Uses SHA1 by default.
def generate_digest(data)
key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
end
private
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
data = ActiveSupport::Base64.encode64s(Marshal.dump(session))
"#{data}--#{generate_digest(data)}"
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
if cookie
data, digest = cookie.split('--')
# Do two checks to transparently support old double-escaped data.
unless digest == generate_digest(data) || digest == generate_digest(data = CGI.unescape(data))
delete
raise TamperedWithCookie
def initialize(app, options = {})
# Process legacy CGI options
options = options.symbolize_keys
if options.has_key?(:session_path)
options[:path] = options.delete(:session_path)
end
if options.has_key?(:session_key)
options[:key] = options.delete(:session_key)
end
if options.has_key?(:session_http_only)
options[:httponly] = options.delete(:session_http_only)
end
Marshal.load(ActiveSupport::Base64.decode64(data))
@app = app
# The session_key option is required.
ensure_session_key(options[:key])
@key = options.delete(:key).freeze
# The secret option is required.
ensure_secret_secure(options[:secret])
@secret = options.delete(:secret).freeze
@digest = options.delete(:digest) || 'SHA1'
@verifier = verifier_for(@secret, @digest)
@default_options = DEFAULT_OPTIONS.merge(options).freeze
freeze
end
end
# Read the session data cookie.
def read_cookie
@session.cgi.cookies[@cookie_options['name']].first
end
def call(env)
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
# CGI likes to make you hack.
def write_cookie(options)
cookie = CGI::Cookie.new(@cookie_options.merge(options))
@session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
end
status, headers, body = @app.call(env)
# Clear cookie value so subsequent new_session doesn't reload old data.
def clear_old_cookie_value
@session.cgi.cookies[@cookie_options['name']].clear
session_data = env[ENV_SESSION_KEY]
options = env[ENV_SESSION_OPTIONS_KEY]
if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
session_data = marshal(session_data.to_hash)
raise CookieOverflow if session_data.size > MAX
cookie = Hash.new
cookie[:value] = session_data
unless options[:expire_after].nil?
cookie[:expires] = Time.now + options[:expire_after]
end
cookie = build_cookie(@key, cookie.merge(options))
unless headers[HTTP_SET_COOKIE].blank?
headers[HTTP_SET_COOKIE] << "\n#{cookie}"
else
headers[HTTP_SET_COOKIE] = cookie
end
end
[status, headers, body]
end
private
# Should be in Rack::Utils soon
def build_cookie(key, value)
case value
when Hash
domain = "; domain=" + value[:domain] if value[:domain]
path = "; path=" + value[:path] if value[:path]
# According to RFC 2109, we need dashes here.
# N.B.: cgi.rb uses spaces...
expires = "; expires=" + value[:expires].clone.gmtime.
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
secure = "; secure" if value[:secure]
httponly = "; HttpOnly" if value[:httponly]
value = value[:value]
end
value = [value] unless Array === value
cookie = Rack::Utils.escape(key) + "=" +
value.map { |v| Rack::Utils.escape(v) }.join("&") +
"#{domain}#{path}#{expires}#{secure}#{httponly}"
end
def load_session(env)
request = Rack::Request.new(env)
session_data = request.cookies[@key]
data = unmarshal(session_data) || persistent_session_id!({})
[data[:session_id], data]
end
# Marshal a session hash into safe cookie data. Include an integrity hash.
def marshal(session)
@verifier.generate(persistent_session_id!(session))
end
# Unmarshal cookie data to a hash and verify its integrity.
def unmarshal(cookie)
persistent_session_id!(@verifier.verify(cookie)) if cookie
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
def ensure_session_key(key)
if key.blank?
raise ArgumentError, 'A key is required to write a ' +
'cookie containing the session data. Use ' +
'config.action_controller.session = { :key => ' +
'"_myapp_session", :secret => "some secret phrase" } in ' +
'config/environment.rb'
end
end
# To prevent users from using something insecure like "Password" we make sure that the
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
# There's no way we can do this check if they've provided a proc for the
# secret.
return true if secret.is_a?(Proc)
if secret.blank?
raise ArgumentError, "A secret is required to generate an " +
"integrity hash for cookie session data. Use " +
"config.action_controller.session = { :key => " +
"\"_myapp_session\", :secret => \"some secret phrase of at " +
"least #{SECRET_MIN_LENGTH} characters\" } " +
"in config/environment.rb"
end
if secret.length < SECRET_MIN_LENGTH
raise ArgumentError, "Secret should be something secure, " +
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
"provided, \"#{secret}\", is shorter than the minimum length " +
"of #{SECRET_MIN_LENGTH} characters"
end
end
def verifier_for(secret, digest)
key = secret.respond_to?(:call) ? secret.call : secret
ActiveSupport::MessageVerifier.new(key, digest)
end
def generate_sid
ActiveSupport::SecureRandom.hex(16)
end
def persistent_session_id!(data)
(data ||= {}).merge!(inject_persistent_session_id(data))
end
def inject_persistent_session_id(data)
requires_session_id?(data) ? { :session_id => generate_sid } : {}
end
def requires_session_id?(data)
if data
data.respond_to?(:key?) && !data.key?(:session_id)
else
true
end
end
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,7 @@
require 'active_support/test_case'
require 'action_controller/test_process'
module ActionController
class NonInferrableControllerError < ActionControllerError
def initialize(name)
@name = name
super "Unable to determine the controller to test from #{name}. " +
"You'll need to specify it using 'tests YourController' in your " +
"test case definition. This could mean that #{inferred_controller_name} does not exist " +
"or it contains syntax errors"
end
def inferred_controller_name
@name.sub(/Test$/, '')
end
end
# Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method. This should not be confused with
# integration tests (see ActionController::IntegrationTest), which are more like
@ -74,7 +61,70 @@ module ActionController
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController
# end
#
# == Testing controller internals
#
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
# * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
# assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
# For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
# appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
# So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
#
# On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating the request collections
#
# The collections described above link to the response, so you can test if what the actions were expected to do happened. But
# sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
# @request.cookies["key"] = "value"
#
# == Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
# Example:
#
# assert_redirected_to page_url(:title => 'foo')
class TestCase < ActiveSupport::TestCase
include TestProcess
def initialize(*args)
super
@controller = nil
end
module Assertions
%w(response selector tag dom routing model).each do |kind|
include ActionController::Assertions.const_get("#{kind.camelize}Assertions")
end
def clean_backtrace(&block)
yield
rescue ActiveSupport::TestCase::Assertion => error
framework_path = Regexp.new(File.expand_path("#{File.dirname(__FILE__)}/assertions"))
error.backtrace.reject! { |line| File.expand_path(line) =~ framework_path }
raise
end
end
include Assertions
# When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
# (bystepping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
# rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
@ -82,17 +132,23 @@ module ActionController
#
# The exception is stored in the exception accessor for further inspection.
module RaiseActionExceptions
attr_accessor :exception
def rescue_action_without_handler(e)
self.exception = e
if request.remote_addr == "0.0.0.0"
raise(e)
else
super(e)
def self.included(base)
base.class_eval do
attr_accessor :exception
protected :exception, :exception=
end
end
protected
def rescue_action_without_handler(e)
self.exception = e
if request.remote_addr == "0.0.0.0"
raise(e)
else
super(e)
end
end
end
setup :setup_controller_request_and_response
@ -107,7 +163,7 @@ module ActionController
end
def controller_class=(new_class)
prepare_controller_class(new_class)
prepare_controller_class(new_class) if new_class
write_inheritable_attribute(:controller_class, new_class)
end
@ -122,7 +178,7 @@ module ActionController
def determine_default_controller_class(name)
name.sub(/Test$/, '').constantize
rescue NameError
raise NonInferrableControllerError.new(name)
nil
end
def prepare_controller_class(new_class)
@ -131,17 +187,23 @@ module ActionController
end
def setup_controller_request_and_response
@controller = self.class.controller_class.new
@controller.request = @request = TestRequest.new
@request = TestRequest.new
@response = TestResponse.new
@controller.params = {}
@controller.send(:initialize_current_url)
if klass = self.class.controller_class
@controller ||= klass.new rescue nil
end
if @controller
@controller.request = @request
@controller.params = {}
@controller.send(:initialize_current_url)
end
end
# Cause the action to be rescued according to the regular rules for rescue_action when the visitor is not local
def rescue_action_in_public!
@request.remote_addr = '208.77.188.166' # example.com
end
end
end
end

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