Merge branch 'bzr/golem' of /Users/distler/Sites/code/instiki

This commit is contained in:
Jacques Distler 2011-02-18 23:39:04 -06:00
commit 63fd1dbbb9
51 changed files with 444 additions and 299 deletions

View file

@ -18,7 +18,7 @@ class Itex
begin begin
require 'nokogiri' require 'nokogiri'
def self.xmlparse(text) def self.xmlparse(text)
Nokogiri::XML(text) { |config| config.options = Nokogiri::XML::ParseOptions::STRICT } Nokogiri::XML(text) { |config| config.strict }
end end
rescue LoadError rescue LoadError
require 'rexml/document' require 'rexml/document'

View file

@ -33,6 +33,7 @@
<%= @web && @web.additional_style ? @web.additional_style.html_safe : '' %> <%= @web && @web.additional_style ? @web.additional_style.html_safe : '' %>
/*]]>*/--></style> /*]]>*/--></style>
<%= javascript_include_tag :defaults %> <%= javascript_include_tag :defaults %>
<%= csrf_meta_tag %>
<%- if @web -%> <%- if @web -%>
<%- if @web.markup == :markdownMML -%> <%- if @web.markup == :markdownMML -%>
<script type="text/javascript"> <script type="text/javascript">

View file

@ -69,9 +69,11 @@ function cleanAuthorName() {
} }
document.forms["editForm"].elements["content"].focus(); document.forms["editForm"].elements["content"].focus();
<%- if [:markdownMML, :markdownPNG, :markdown].include?(@web.markup) and !@page.categories.include?('S5-slideshow') -%> <%- if [:markdownMML, :markdownPNG, :markdown].include?(@web.markup) -%>
setupSVGedit('<%= compute_public_path("editor/svg-editor.html", "svg-edit").split(/\?/)[0] %>'); setupSVGedit('<%= compute_public_path("editor/svg-editor.html", "svg-edit").split(/\?/)[0] %>');
<%- unless @page.categories.include?('S5-slideshow') -%>
addS5button('<%= @page.name.escapeHTML %>'); addS5button('<%= @page.name.escapeHTML %>');
<%- end -%> <%- end -%>
<%- end -%>
//--><!]]> //--><!]]>
</script> </script>

View file

@ -5,3 +5,25 @@ function toggleView(id)
(document.getElementById(id).style.display == 'block') ? document.getElementById(id).style.display='none' : document.getElementById(id).style.display='block'; (document.getElementById(id).style.display == 'block') ? document.getElementById(id).style.display='none' : document.getElementById(id).style.display='block';
} }
/*
* Registers a callback which copies the csrf token into the
* X-CSRF-Token header with each ajax request. Necessary to
* work with rails applications which have fixed
* CVE-2011-0447
*/
Ajax.Responders.register({
onCreate: function(request) {
var csrf_meta_tag = $$('meta[name=csrf-token]')[0];
if (csrf_meta_tag) {
var header = 'X-CSRF-Token',
token = csrf_meta_tag.readAttribute('content');
if (!request.options.requestHeaders) {
request.options.requestHeaders = {};
}
request.options.requestHeaders[header] = token;
}
}
});

View file

@ -1,3 +1,4 @@
*2.3.11 (February 9, 2011)*
*2.3.10 (October 15, 2010)* *2.3.10 (October 15, 2010)*
*2.3.9 (September 4, 2010)* *2.3.9 (September 4, 2010)*
*2.3.8 (May 24, 2010)* *2.3.8 (May 24, 2010)*

View file

@ -54,7 +54,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.3.10' + PKG_BUILD) s.add_dependency('actionpack', '= 2.3.11' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'

View file

@ -195,6 +195,39 @@ module ActionMailer #:nodoc:
# end # end
# end # end
# #
# = Multipart Emails with Attachments
#
# Multipart emails that also have attachments can be created by nesting a "multipart/alternative" part
# within an email that has its content type set to "multipart/mixed". This would also need two templates
# in place within +app/views/mailer+ called "welcome_email.text.html.erb" and "welcome_email.text.plain.erb"
#
# class ApplicationMailer < ActionMailer::Base
# def signup_notification(recipient)
# recipients recipient.email_address_with_name
# subject "New account information"
# from "system@example.com"
# content_type "multipart/mixed"
#
# part "multipart/alternative" do |alternative|
#
# alternative.part "text/html" do |html|
# html.body = render_message("welcome_email.text.html", :message => "<h1>HTML content</h1>")
# end
#
# alternative.part "text/plain" do |plain|
# plain.body = render_message("welcome_email.text.plain", :message => "text content")
# end
#
# end
#
# attachment :content_type => "image/png",
# :body => File.read(File.join(RAILS_ROOT, 'public/images/rails.png'))
#
# attachment "application/pdf" do |a|
# a.body = File.read('/Users/mikel/Code/mail/spec/fixtures/attachments/test.pdf')
# end
# end
# end
# #
# = Configuration options # = Configuration options
# #

View file

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

View file

@ -1,3 +1,7 @@
*2.3.11 (February 9, 2011)*
* Two security fixes. CVE-2011-0446, CVE-2011-0447
*2.3.10 (October 15, 2010)* *2.3.10 (October 15, 2010)*
*2.3.9 (September 4, 2010)* *2.3.9 (September 4, 2010)*

View file

@ -79,7 +79,7 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 2.3.10' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.11' + PKG_BUILD)
s.add_dependency('rack', '~> 1.1.0') s.add_dependency('rack', '~> 1.1.0')
s.require_path = 'lib' s.require_path = 'lib'

View file

@ -60,7 +60,7 @@ module ActionController #:nodoc:
attr_reader :controller attr_reader :controller
def initialize(controller) def initialize(controller)
@controller, @cookies = controller, controller.request.cookies @controller, @cookies, @secure = controller, controller.request.cookies, controller.request.ssl?
super() super()
update(@cookies) update(@cookies)
end end
@ -81,7 +81,7 @@ module ActionController #:nodoc:
options[:path] = "/" unless options.has_key?(:path) options[:path] = "/" unless options.has_key?(:path)
super(key.to_s, options[:value]) super(key.to_s, options[:value])
@controller.response.set_cookie(key, options) @controller.response.set_cookie(key, options) if write_cookie?(options)
end end
# Removes the cookie on the client machine by setting the value to an empty string # Removes the cookie on the client machine by setting the value to an empty string
@ -126,6 +126,12 @@ module ActionController #:nodoc:
def signed def signed
@signed ||= SignedCookieJar.new(self) @signed ||= SignedCookieJar.new(self)
end end
private
def write_cookie?(cookie)
@secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development?
end
end end
class PermanentCookieJar < CookieJar #:nodoc: class PermanentCookieJar < CookieJar #:nodoc:

View file

@ -76,7 +76,11 @@ module ActionController #:nodoc:
protected protected
# The actual before_filter that is used. Modify this to change how you handle unverified requests. # The actual before_filter that is used. Modify this to change how you handle unverified requests.
def verify_authenticity_token def verify_authenticity_token
verified_request? || raise(ActionController::InvalidAuthenticityToken) verified_request? || handle_unverified_request
end
def handle_unverified_request
reset_session
end end
# Returns true or false if a request is verified. Checks: # Returns true or false if a request is verified. Checks:
@ -85,11 +89,10 @@ module ActionController #:nodoc:
# * is it a GET request? Gets should be safe and idempotent # * is it a GET request? Gets should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params? # * Does the form_authenticity_token match the given token value from the params?
def verified_request? def verified_request?
!protect_against_forgery? || !protect_against_forgery? ||
request.method == :get || request.get? ||
request.xhr? || form_authenticity_token == form_authenticity_param ||
!verifiable_request_format? || form_authenticity_token == request.headers['X-CSRF-Token']
form_authenticity_token == form_authenticity_param
end end
def form_authenticity_param def form_authenticity_param

View file

@ -195,22 +195,8 @@ module ActionController
request_cookies = env["rack.request.cookie_hash"] request_cookies = env["rack.request.cookie_hash"]
if (request_cookies.nil? || request_cookies[@key] != sid) || options[:expire_after] if (request_cookies.nil? || request_cookies[@key] != sid) || options[:expire_after]
cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid) cookie = {:value => sid}
cookie << "; domain=#{options[:domain]}" if options[:domain] Rack::Utils.set_cookie_header!(response[1], @key, cookie.merge(options))
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 end
end end

View file

@ -52,7 +52,6 @@ module ActionController
ENV_SESSION_KEY = "rack.session".freeze ENV_SESSION_KEY = "rack.session".freeze
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
HTTP_SET_COOKIE = "Set-Cookie".freeze
# Raised when storing more than 4K of session data. # Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end class CookieOverflow < StandardError; end
@ -116,9 +115,7 @@ module ActionController
cookie[:expires] = Time.now + options[:expire_after] cookie[:expires] = Time.now + options[:expire_after]
end end
cookie = build_cookie(@key, cookie.merge(options)) Rack::Utils.set_cookie_header!(headers, @key, cookie.merge(options))
headers[HTTP_SET_COOKIE] = [] if headers[HTTP_SET_COOKIE].blank?
headers[HTTP_SET_COOKIE] << cookie
end end
[status, headers, body] [status, headers, body]
@ -131,26 +128,6 @@ module ActionController
env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options) env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options)
end end
# 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) def load_session(env)
data = unpacked_cookie_data(env) data = unpacked_cookie_data(env)
data = persistent_session_id!(data) data = persistent_session_id!(data)

View file

@ -1,6 +1,6 @@
begin begin
require_library_or_gem 'memcache' require_library_or_gem 'memcache'
require 'thread'
module ActionController module ActionController
module Session module Session
class MemCacheStore < AbstractStore class MemCacheStore < AbstractStore

View file

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

View file

@ -6,6 +6,7 @@ module ActionView #:nodoc:
autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper' autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper'
autoload :CacheHelper, 'action_view/helpers/cache_helper' autoload :CacheHelper, 'action_view/helpers/cache_helper'
autoload :CaptureHelper, 'action_view/helpers/capture_helper' autoload :CaptureHelper, 'action_view/helpers/capture_helper'
autoload :CsrfHelper, 'action_view/helpers/csrf_helper'
autoload :DateHelper, 'action_view/helpers/date_helper' autoload :DateHelper, 'action_view/helpers/date_helper'
autoload :DebugHelper, 'action_view/helpers/debug_helper' autoload :DebugHelper, 'action_view/helpers/debug_helper'
autoload :FormHelper, 'action_view/helpers/form_helper' autoload :FormHelper, 'action_view/helpers/form_helper'
@ -38,6 +39,7 @@ module ActionView #:nodoc:
include BenchmarkHelper include BenchmarkHelper
include CacheHelper include CacheHelper
include CaptureHelper include CaptureHelper
include CsrfHelper
include DateHelper include DateHelper
include DebugHelper include DebugHelper
include FormHelper include FormHelper

View file

@ -1,6 +1,7 @@
require 'cgi' require 'cgi'
require 'action_view/helpers/url_helper' require 'action_view/helpers/url_helper'
require 'action_view/helpers/tag_helper' require 'action_view/helpers/tag_helper'
require 'thread'
module ActionView module ActionView
module Helpers #:nodoc: module Helpers #:nodoc:

View file

@ -0,0 +1,14 @@
module ActionView
# = Action View CSRF Helper
module Helpers
module CsrfHelper
# Returns a meta tag with the cross-site request forgery protection token
# for forms to use. Place this in your head.
def csrf_meta_tag
if protect_against_forgery?
%(<meta name="csrf-param" content="#{h(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{h(form_authenticity_token)}"/>).html_safe
end
end
end
end
end

View file

@ -665,7 +665,7 @@ module ActionView
# #
# The HTML specification says unchecked check boxes are not successful, and # The HTML specification says unchecked check boxes are not successful, and
# thus web browsers do not send them. Unfortunately this introduces a gotcha: # thus web browsers do not send them. Unfortunately this introduces a gotcha:
# if an Invoice model has a +paid+ flag, and in the form that edits a paid # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
# invoice the user unchecks its check box, no +paid+ parameter is sent. So, # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
# any mass-assignment idiom like # any mass-assignment idiom like
# #
@ -673,12 +673,15 @@ module ActionView
# #
# wouldn't update the flag. # wouldn't update the flag.
# #
# To prevent this the helper generates a hidden field with the same name as # To prevent this the helper generates an auxiliary hidden field before
# the checkbox after the very check box. So, the client either sends only the # the very check box. The hidden field has the same name and its
# hidden field (representing the check box is unchecked), or both fields. # attributes mimick an unchecked check box.
# Since the HTML specification says key/value pairs have to be sent in the #
# same order they appear in the form and Rails parameters extraction always # This way, the client either sends only the hidden field (representing
# gets the first occurrence of any given key, that works in ordinary forms. # the check box is unchecked), or both fields. Since the HTML specification
# says key/value pairs have to be sent in the same order they appear in the
# form, and parameters extraction gets the last occurrence of any repeated
# key in the query string, that works for ordinary forms.
# #
# Unfortunately that workaround does not work when the check box goes # Unfortunately that workaround does not work when the check box goes
# within an array-like parameter, as in # within an array-like parameter, as in
@ -689,22 +692,26 @@ module ActionView
# <% end %> # <% end %>
# #
# because parameter name repetition is precisely what Rails seeks to distinguish # because parameter name repetition is precisely what Rails seeks to distinguish
# the elements of the array. # the elements of the array. For each item with a checked check box you
# get an extra ghost item with only that attribute, assigned to "0".
#
# In that case it is preferable to either use +check_box_tag+ or to use
# hashes instead of arrays.
# #
# ==== Examples # ==== Examples
# # Let's say that @post.validated? is 1: # # Let's say that @post.validated? is 1:
# check_box("post", "validated") # check_box("post", "validated")
# # => <input type="checkbox" id="post_validated" name="post[validated]" value="1" /> # # => <input name="post[validated]" type="hidden" value="0" />
# # <input name="post[validated]" type="hidden" value="0" /> # # <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
# #
# # Let's say that @puppy.gooddog is "no": # # Let's say that @puppy.gooddog is "no":
# check_box("puppy", "gooddog", {}, "yes", "no") # check_box("puppy", "gooddog", {}, "yes", "no")
# # => <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> # # => <input name="puppy[gooddog]" type="hidden" value="no" />
# # <input name="puppy[gooddog]" type="hidden" value="no" /> # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
# #
# check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no") # check_box("eula", "accepted", { :class => 'eula_check' }, "yes", "no")
# # => <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> # # => <input name="eula[accepted]" type="hidden" value="no" />
# # <input name="eula[accepted]" type="hidden" value="no" /> # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
# #
def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)

View file

@ -471,7 +471,8 @@ module ActionView
email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot")
if encode == "javascript" if encode == "javascript"
"document.write('#{content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');".each_byte do |c| html = content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge({ "href" => "mailto:"+html_escape(email_address)+extras }))
"document.write('#{escape_javascript(html)}');".each_byte do |c|
string << sprintf("%%%x", c) string << sprintf("%%%x", c)
end end
"<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>" "<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>"

View file

@ -27,7 +27,7 @@ module ActionView
def render_partial(view, object = nil, local_assigns = {}, as = nil) def render_partial(view, object = nil, local_assigns = {}, as = nil)
object ||= local_assigns[:object] || local_assigns[variable_name] object ||= local_assigns[:object] || local_assigns[variable_name]
if object.nil? && view.respond_to?(:controller) if object.nil? && !local_assigns_key?(local_assigns) && view.respond_to?(:controller)
ivar = :"@#{variable_name}" ivar = :"@#{variable_name}"
object = object =
if view.controller.instance_variable_defined?(ivar) if view.controller.instance_variable_defined?(ivar)
@ -43,5 +43,11 @@ module ActionView
render_template(view, local_assigns) render_template(view, local_assigns)
end end
private
def local_assigns_key?(local_assigns)
local_assigns.key?(:object) || local_assigns.key?(variable_name)
end
end end
end end

View file

@ -100,11 +100,26 @@ class CookieTest < ActionController::TestCase
end end
def test_setting_cookie_with_secure def test_setting_cookie_with_secure
@request.env["HTTPS"] = "on"
get :authenticate_with_secure get :authenticate_with_secure
assert_equal ["user_name=david; path=/; secure"], @response.headers["Set-Cookie"] assert_equal ["user_name=david; path=/; secure"], @response.headers["Set-Cookie"]
assert_equal({"user_name" => "david"}, @response.cookies) assert_equal({"user_name" => "david"}, @response.cookies)
end end
def test_setting_cookie_with_secure_in_development
with_environment(:development) do
get :authenticate_with_secure
assert_equal ["user_name=david; path=/; secure"], @response.headers["Set-Cookie"]
assert_equal({"user_name" => "david"}, @response.cookies)
end
end
def test_not_setting_cookie_with_secure
get :authenticate_with_secure
assert_not_equal ["user_name=david; path=/; secure"], @response.headers["Set-Cookie"]
assert_not_equal({"user_name" => "david"}, @response.cookies)
end
def test_multiple_cookies def test_multiple_cookies
get :set_multiple_cookies get :set_multiple_cookies
assert_equal 2, @response.cookies.size assert_equal 2, @response.cookies.size
@ -177,4 +192,17 @@ class CookieTest < ActionController::TestCase
assert_match %r(#{20.years.from_now.year}), @response.headers["Set-Cookie"].first assert_match %r(#{20.years.from_now.year}), @response.headers["Set-Cookie"].first
assert_equal 100, @controller.send(:cookies).signed[:remember_me] assert_equal 100, @controller.send(:cookies).signed[:remember_me]
end end
private
def with_environment(enviroment)
old_rails = Object.const_get(:Rails) rescue nil
mod = Object.const_set(:Rails, Module.new)
(class << mod; self; end).instance_eval do
define_method(:env) { @_env ||= ActiveSupport::StringInquirer.new(enviroment.to_s) }
end
yield
ensure
Object.module_eval { remove_const(:Rails) } if defined?(Rails)
Object.const_set(:Rails, old_rails) if old_rails
end
end end

View file

@ -1,4 +1,5 @@
require 'abstract_unit' require 'abstract_unit'
require 'thread'
class ReloaderTests < ActiveSupport::TestCase class ReloaderTests < ActiveSupport::TestCase
Reloader = ActionController::Reloader Reloader = ActionController::Reloader

View file

@ -716,6 +716,11 @@ class TestController < ActionController::Base
render :partial => "customer" render :partial => "customer"
end end
def partial_with_implicit_local_assignment_and_nil_local
@customer = Customer.new("Marcel")
render :partial => "customer", :locals => { :customer => nil }
end
def render_call_to_partial_with_layout def render_call_to_partial_with_layout
render :action => "calling_partial_with_layout" render :action => "calling_partial_with_layout"
end end
@ -1543,6 +1548,13 @@ class RenderTest < ActionController::TestCase
end end
end end
def test_partial_with_implicit_local_assignment_and_nil_local
assert_not_deprecated do
get :partial_with_implicit_local_assignment_and_nil_local
assert_equal "Hello: Anonymous", @response.body
end
end
def test_render_missing_partial_template def test_render_missing_partial_template
assert_raise(ActionView::MissingTemplate) do assert_raise(ActionView::MissingTemplate) do
get :missing_partial get :missing_partial

View file

@ -23,6 +23,10 @@ module RequestForgeryProtectionActions
render :text => 'pwn' render :text => 'pwn'
end end
def meta
render :inline => "<%= csrf_meta_tag %>"
end
def rescue_action(e) raise e end def rescue_action(e) raise e end
end end
@ -32,6 +36,16 @@ class RequestForgeryProtectionController < ActionController::Base
protect_from_forgery :only => :index protect_from_forgery :only => :index
end end
class RequestForgeryProtectionControllerUsingOldBehaviour < ActionController::Base
include RequestForgeryProtectionActions
protect_from_forgery :only => %w(index meta)
def handle_unverified_request
raise(ActionController::InvalidAuthenticityToken)
end
end
class FreeCookieController < RequestForgeryProtectionController class FreeCookieController < RequestForgeryProtectionController
self.allow_forgery_protection = false self.allow_forgery_protection = false
@ -54,158 +68,92 @@ end
# common test methods # common test methods
module RequestForgeryProtectionTests module RequestForgeryProtectionTests
def teardown def setup
ActionController::Base.request_forgery_protection_token = nil @token = "cf50faa3fe97702ca1ae"
ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
ActionController::Base.request_forgery_protection_token = :authenticity_token
end end
def test_should_render_form_with_token_tag def test_should_render_form_with_token_tag
get :index assert_not_blocked do
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token get :index
end
def test_should_render_button_to_with_token_tag
get :show_button
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
end
def test_should_render_remote_form_with_only_one_token_parameter
get :remote_form
assert_equal 1, @response.body.scan(@token).size
end
def test_should_allow_get
get :index
assert_response :success
end
def test_should_allow_post_without_token_on_unsafe_action
post :unsafe
assert_response :success
end
def test_should_not_allow_html_post_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
assert_raise(ActionController::InvalidAuthenticityToken) { post :index, :format => :html }
end
def test_should_not_allow_html_put_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
assert_raise(ActionController::InvalidAuthenticityToken) { put :index, :format => :html }
end
def test_should_not_allow_html_delete_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
assert_raise(ActionController::InvalidAuthenticityToken) { delete :index, :format => :html }
end
def test_should_allow_api_formatted_post_without_token
assert_nothing_raised do
post :index, :format => 'xml'
end end
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
end end
def test_should_not_allow_api_formatted_put_without_token def test_should_render_button_to_with_token_tag
assert_nothing_raised do assert_not_blocked do
put :index, :format => 'xml' get :show_button
end end
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
end end
def test_should_allow_api_formatted_delete_without_token def test_should_allow_get
assert_nothing_raised do assert_not_blocked { get :index }
delete :index, :format => 'xml'
end
end end
def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token def test_should_allow_post_without_token_on_unsafe_action
assert_raise(ActionController::InvalidAuthenticityToken) do assert_not_blocked { post :unsafe }
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
post :index, :format => 'xml'
end
end end
def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token def test_should_not_allow_post_without_token
assert_raise(ActionController::InvalidAuthenticityToken) do assert_blocked { post :index }
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
put :index, :format => 'xml'
end
end end
def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token def test_should_not_allow_post_without_token_irrespective_of_format
assert_raise(ActionController::InvalidAuthenticityToken) do assert_blocked { post :index, :format=>'xml' }
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
delete :index, :format => 'xml'
end
end end
def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token def test_should_not_allow_put_without_token
assert_raise(ActionController::InvalidAuthenticityToken) do assert_blocked { put :index }
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
post :index, :format => 'xml'
end
end end
def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token def test_should_not_allow_delete_without_token
assert_raise(ActionController::InvalidAuthenticityToken) do assert_blocked { delete :index }
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
put :index, :format => 'xml'
end
end end
def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token def test_should_not_allow_xhr_post_without_token
assert_raise(ActionController::InvalidAuthenticityToken) do assert_blocked { xhr :post, :index }
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
delete :index, :format => 'xml'
end
end
def test_should_allow_xhr_post_without_token
assert_nothing_raised { xhr :post, :index }
end
def test_should_allow_xhr_put_without_token
assert_nothing_raised { xhr :put, :index }
end
def test_should_allow_xhr_delete_without_token
assert_nothing_raised { xhr :delete, :index }
end
def test_should_allow_xhr_post_with_encoded_form_content_type_without_token
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
assert_nothing_raised { xhr :post, :index }
end end
def test_should_allow_post_with_token def test_should_allow_post_with_token
post :index, :authenticity_token => @token assert_not_blocked { post :index, :authenticity_token => @token }
assert_response :success
end end
def test_should_allow_put_with_token def test_should_allow_put_with_token
put :index, :authenticity_token => @token assert_not_blocked { put :index, :authenticity_token => @token }
assert_response :success
end end
def test_should_allow_delete_with_token def test_should_allow_delete_with_token
delete :index, :authenticity_token => @token assert_not_blocked { delete :index, :authenticity_token => @token }
end
def test_should_allow_post_with_token_in_header
@request.env['HTTP_X_CSRF_TOKEN'] = @token
assert_not_blocked { post :index }
end
def test_should_allow_delete_with_token_in_header
@request.env['HTTP_X_CSRF_TOKEN'] = @token
assert_not_blocked { delete :index }
end
def test_should_allow_put_with_token_in_header
@request.env['HTTP_X_CSRF_TOKEN'] = @token
assert_not_blocked { put :index }
end
def assert_blocked
session[:something_like_user_id] = 1
yield
assert_nil session[:something_like_user_id], "session values are still present"
assert_response :success assert_response :success
end end
def test_should_allow_post_with_xml def assert_not_blocked
@request.env['CONTENT_TYPE'] = Mime::XML.to_s assert_nothing_raised { yield }
post :index, :format => 'xml'
assert_response :success
end
def test_should_allow_put_with_xml
@request.env['CONTENT_TYPE'] = Mime::XML.to_s
put :index, :format => 'xml'
assert_response :success
end
def test_should_allow_delete_with_xml
@request.env['CONTENT_TYPE'] = Mime::XML.to_s
delete :index, :format => 'xml'
assert_response :success assert_response :success
end end
end end
@ -214,15 +162,20 @@ end
class RequestForgeryProtectionControllerTest < ActionController::TestCase class RequestForgeryProtectionControllerTest < ActionController::TestCase
include RequestForgeryProtectionTests include RequestForgeryProtectionTests
def setup
@controller = RequestForgeryProtectionController.new
@request = ActionController::TestRequest.new
@request.format = :html
@response = ActionController::TestResponse.new
@token = "cf50faa3fe97702ca1ae"
ActiveSupport::SecureRandom.stubs(:base64).returns(@token) test 'should emit a csrf-token meta tag' do
ActionController::Base.request_forgery_protection_token = :authenticity_token ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
get :meta
assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae&lt;=?"/>), @response.body
end
end
class RequestForgeryProtectionControllerUsingOldBehaviourTest < ActionController::TestCase
include RequestForgeryProtectionTests
def assert_blocked
assert_raises(ActionController::InvalidAuthenticityToken) do
yield
end
end end
end end
@ -251,15 +204,30 @@ class FreeCookieControllerTest < ActionController::TestCase
assert_nothing_raised { send(method, :index)} assert_nothing_raised { send(method, :index)}
end end
end end
test 'should not emit a csrf-token meta tag' do
get :meta
assert_blank @response.body
end
end end
class CustomAuthenticityParamControllerTest < ActionController::TestCase class CustomAuthenticityParamControllerTest < ActionController::TestCase
def setup def setup
ActionController::Base.request_forgery_protection_token = :custom_token_name
super
end
def teardown
ActionController::Base.request_forgery_protection_token = :authenticity_token ActionController::Base.request_forgery_protection_token = :authenticity_token
super
end end
def test_should_allow_custom_token def test_should_allow_custom_token
post :index, :authenticity_token => 'foobar' post :index, :custom_token_name => 'foobar'
assert_response :ok assert_response :ok
end end
end end

View file

@ -106,7 +106,7 @@ class CookieStoreTest < ActionController::IntegrationTest
with_test_route_set do with_test_route_set do
get '/set_session_value' get '/set_session_value'
assert_response :success assert_response :success
assert_equal ["_myapp_session=#{response.body}; path=/; HttpOnly"], assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
headers['Set-Cookie'] headers['Set-Cookie']
end end
end end
@ -159,7 +159,7 @@ class CookieStoreTest < ActionController::IntegrationTest
with_test_route_set(:secure => true) do with_test_route_set(:secure => true) do
get '/set_session_value', nil, 'HTTPS' => 'on' get '/set_session_value', nil, 'HTTPS' => 'on'
assert_response :success assert_response :success
assert_equal ["_myapp_session=#{response.body}; path=/; secure; HttpOnly"], assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly",
headers['Set-Cookie'] headers['Set-Cookie']
end end
end end
@ -195,12 +195,12 @@ class CookieStoreTest < ActionController::IntegrationTest
get '/set_session_value' get '/set_session_value'
assert_response :success assert_response :success
session_payload = response.body session_payload = response.body
assert_equal ["_myapp_session=#{response.body}; path=/; HttpOnly"], assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
headers['Set-Cookie'] headers['Set-Cookie']
get '/call_reset_session' get '/call_reset_session'
assert_response :success assert_response :success
assert_not_equal [], headers['Set-Cookie'] assert_not_equal "", headers['Set-Cookie']
assert_not_equal session_payload, cookies[SessionKey] assert_not_equal session_payload, cookies[SessionKey]
get '/get_session_value' get '/get_session_value'
@ -214,7 +214,7 @@ class CookieStoreTest < ActionController::IntegrationTest
get '/set_session_value' get '/set_session_value'
assert_response :success assert_response :success
session_payload = response.body session_payload = response.body
assert_equal ["_myapp_session=#{response.body}; path=/; HttpOnly"], assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
headers['Set-Cookie'] headers['Set-Cookie']
get '/call_session_clear' get '/call_session_clear'

View file

@ -333,11 +333,11 @@ class UrlHelperTest < ActionView::TestCase
end end
def test_mail_to_with_javascript def test_mail_to_with_javascript
assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript") assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript")
end end
def test_mail_to_with_javascript_unicode def test_mail_to_with_javascript_unicode
assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%22%3e%c3%ba%6e%69%63%6f%64%65%3c%2f%61%3e%27%29%3b'))</script>", mail_to("unicode@example.com", "únicode", :encode => "javascript") assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%5c%22%3e%c3%ba%6e%69%63%6f%64%65%3c%5c%2f%61%3e%27%29%3b'))</script>", mail_to("unicode@example.com", "únicode", :encode => "javascript")
end end
def test_mail_with_options def test_mail_with_options
@ -361,8 +361,8 @@ class UrlHelperTest < ActionView::TestCase
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)")
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)") assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">My email</a>", mail_to("me@domain.com", "My email", :encode => "hex", :replace_at => "(at)")
assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#40;&#100;&#111;&#116;&#41;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)") assert_dom_equal "<a href=\"&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#40;&#100;&#111;&#116;&#41;&#99;&#111;&#109;</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)", :replace_dot => "(dot)")
assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%4d%79%20%65%6d%61%69%6c%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", "My email", :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)") assert_dom_equal "<script type=\"text/javascript\">eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%5c%2f%61%3e%27%29%3b'))</script>", mail_to("me@domain.com", nil, :encode => "javascript", :replace_at => "(at)", :replace_dot => "(dot)")
end end
def protect_against_forgery? def protect_against_forgery?

View file

@ -1,3 +1,5 @@
*2.3.11 (February 9, 2011)*
*2.3.10 (October 15, 2010)* *2.3.10 (October 15, 2010)*
* Security Release to fix CVE-2010-3933 * Security Release to fix CVE-2010-3933

View file

@ -192,7 +192,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end end
s.add_dependency('activesupport', '= 2.3.10' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.11' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite" s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite" s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"

View file

@ -353,15 +353,14 @@ module ActiveRecord
if @target.is_a?(Array) && @target.any? if @target.is_a?(Array) && @target.any?
@target = find_target.map do |f| @target = find_target.map do |f|
i = @target.index(f) i = @target.index(f)
if i t = @target.delete_at(i) if i
@target.delete_at(i).tap do |t| if t && t.changed?
keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) t
t.attributes = f.attributes.except(*keys)
end
else else
f.mark_for_destruction if t && t.marked_for_destruction?
f f
end end
end + @target end + @target.find_all {|t| t.new_record?}
else else
@target = find_target @target = find_target
end end
@ -375,16 +374,17 @@ module ActiveRecord
target target
end end
def method_missing(method, *args) def method_missing(method, *args, &block)
case method.to_s case method.to_s
when 'find_or_create' when 'find_or_create'
return find(:first, :conditions => args.first) || create(args.first) return find(:first, :conditions => args.first) || create(args.first)
when /^find_or_create_by_(.*)$/ when /^find_or_create_by_(.*)$/
rest = $1 rest = $1
return send("find_by_#{rest}", *args) || find_args = pull_finder_args_from(DynamicFinderMatch.match(method).attribute_names, *args)
method_missing("create_by_#{rest}", *args) return send("find_by_#{rest}", find_args) ||
method_missing("create_by_#{rest}", *args, &block)
when /^create_by_(.*)$/ when /^create_by_(.*)$/
return create($1.split('_and_').zip(args).inject({}) { |h,kv| k,v=kv ; h[k] = v ; h }) return create($1.split('_and_').zip(args).inject({}) { |h,kv| k,v=kv ; h[k] = v ; h }, &block)
end end
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
@ -434,20 +434,32 @@ module ActiveRecord
callback(:before_add, record) callback(:before_add, record)
yield(record) if block_given? yield(record) if block_given?
@target ||= [] unless loaded? @target ||= [] unless loaded?
index = @target.index(record) @target << record unless @reflection.options[:uniq] && @target.include?(record)
unless @reflection.options[:uniq] && index
if index
@target[index] = record
else
@target << record
end
end
callback(:after_add, record) callback(:after_add, record)
set_inverse_instance(record, @owner) set_inverse_instance(record, @owner)
record record
end end
private private
# Separate the "finder" args from the "create" args given to a
# find_or_create_by_ call. Returns an array with the
# parameter values in the same order as the keys in the
# "names" array. This code was based on code in base.rb's
# method_missing method.
def pull_finder_args_from(names, *args)
attributes = names.collect { |name| name.intern }
attribute_hash = {}
args.each_with_index do |arg, i|
if arg.is_a?(Hash)
attribute_hash.merge! arg
else
attribute_hash[attributes[i]] = arg
end
end
attribute_hash = attribute_hash.with_indifferent_access
attributes.collect { |attr| attribute_hash[attr] }
end
def create_record(attrs) def create_record(attrs)
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
ensure_owner_is_not_new ensure_owner_is_not_new

View file

@ -187,7 +187,7 @@ module ActiveRecord
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] options[:group_fields].each_index{|i| sql << ", #{options[:group_fields][i]} AS #{options[:group_aliases][i]}" } if options[:group]
if options[:from] if options[:from]
sql << " FROM #{options[:from]} " sql << " FROM #{options[:from]} "
elsif scope && scope[:from] && !use_workaround elsif scope && scope[:from] && !use_workaround
@ -211,8 +211,8 @@ module ActiveRecord
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
if options[:group] if options[:group]
group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field group_key = connection.adapter_name == 'FrontBase' ? :group_aliases : :group_fields
sql << " GROUP BY #{options[group_key]} " sql << " GROUP BY #{options[group_key].join(',')} "
end end
if options[:group] && options[:having] if options[:group] && options[:having]
@ -239,24 +239,31 @@ module ActiveRecord
end end
def execute_grouped_calculation(operation, column_name, column, options) #:nodoc: def execute_grouped_calculation(operation, column_name, column, options) #:nodoc:
group_attr = options[:group].to_s group_attr = options[:group]
association = reflect_on_association(group_attr.to_sym) association = reflect_on_association(group_attr.to_s.to_sym)
associated = association && association.macro == :belongs_to # only count belongs_to associations associated = association && association.macro == :belongs_to # only count belongs_to associations
group_field = associated ? association.primary_key_name : group_attr group_fields = Array(associated ? association.primary_key_name : group_attr)
group_alias = column_alias_for(group_field) group_aliases = []
group_column = column_for group_field group_columns = {}
sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias))
group_fields.each do |field|
group_aliases << column_alias_for(field)
group_columns[column_alias_for(field)] = column_for(field)
end
sql = construct_calculation_sql(operation, column_name, options.merge(:group_fields => group_fields, :group_aliases => group_aliases))
calculated_data = connection.select_all(sql) calculated_data = connection.select_all(sql)
aggregate_alias = column_alias_for(operation, column_name) aggregate_alias = column_alias_for(operation, column_name)
if association if association
key_ids = calculated_data.collect { |row| row[group_alias] } key_ids = calculated_data.collect { |row| row[group_aliases.first] }
key_records = association.klass.base_class.find(key_ids) key_records = association.klass.base_class.find(key_ids)
key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
end end
calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
key = type_cast_calculated_value(row[group_alias], group_column) key = group_aliases.map{|group_alias| type_cast_calculated_value(row[group_alias], group_columns[group_alias])}
key = key.first if key.size == 1
key = key_records[key] if associated key = key_records[key] if associated
value = row[aggregate_alias] value = row[aggregate_alias]
all[key] = type_cast_calculated_value(value, column, operation) all[key] = type_cast_calculated_value(value, column, operation)

View file

@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 3 MINOR = 3
TINY = 10 TINY = 11
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -65,6 +65,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal person, person.readers.first.person assert_equal person, person.readers.first.person
end end
def test_find_or_create_by_with_additional_parameters
post = Post.create! :title => 'test_find_or_create_by_with_additional_parameters', :body => 'this is the body'
comment = post.comments.create! :body => 'test comment body', :type => 'test'
assert_equal comment, post.comments.find_or_create_by_body('test comment body')
post.comments.find_or_create_by_body(:body => 'other test comment body', :type => 'test')
assert_equal 2, post.comments.count
assert_equal 2, post.comments.length
post.comments.find_or_create_by_body('other other test comment body', :type => 'test')
assert_equal 3, post.comments.count
assert_equal 3, post.comments.length
post.comments.find_or_create_by_body_and_type('3rd test comment body', 'test')
assert_equal 4, post.comments.count
assert_equal 4, post.comments.length
end
def test_find_or_create_by_with_block
post = Post.create! :title => 'test_find_or_create_by_with_additional_parameters', :body => 'this is the body'
comment = post.comments.find_or_create_by_body('other test comment body') { |comment| comment.type = 'test' }
assert_equal 'test', comment.type
end
def test_find_or_create def test_find_or_create
person = Person.create! :first_name => 'tenderlove' person = Person.create! :first_name => 'tenderlove'
post = Post.find :first post = Post.find :first
@ -843,6 +866,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
end end
def test_destroy_all_with_creates_and_scope_that_doesnt_match_created_records
company = companies(:first_firm)
unloaded_client_matching_scope = companies(:second_client)
created_client_matching_scope = company.clients_of_firm.create!(:name => "Somesoft")
created_client_not_matching_scope = company.clients_of_firm.create!(:name => "OtherCo")
destroyed = company.clients_of_firm.with_oft_in_name.destroy_all
assert destroyed.include?(unloaded_client_matching_scope), "unloaded clients matching the scope destroy_all on should have been destroyed"
assert destroyed.include?(created_client_matching_scope), "loaded clients matching the scope destroy_all on should have been destroyed"
assert !destroyed.include?(created_client_not_matching_scope), "loaded clients not matching the scope destroy_all on should not have been destroyed"
end
def test_dependence def test_dependence
firm = companies(:first_firm) firm = companies(:first_firm)
assert_equal 2, firm.clients.size assert_equal 2, firm.clients.size

View file

@ -18,8 +18,6 @@ require 'models/person'
require 'models/reader' require 'models/reader'
require 'models/parrot' require 'models/parrot'
require 'models/pirate' require 'models/pirate'
require 'models/ship'
require 'models/ship_part'
require 'models/treasure' require 'models/treasure'
require 'models/price_estimate' require 'models/price_estimate'
require 'models/club' require 'models/club'
@ -31,23 +29,6 @@ class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects, fixtures :accounts, :companies, :developers, :projects, :developers_projects,
:computers, :people, :readers :computers, :people, :readers
def test_loading_the_association_target_should_keep_child_records_marked_for_destruction
ship = Ship.create!(:name => "The good ship Dollypop")
part = ship.parts.create!(:name => "Mast")
part.mark_for_destruction
ship.parts.send(:load_target)
assert ship.parts[0].marked_for_destruction?
end
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
ship = Ship.create!(:name => "The good ship Dollypop")
part = ship.parts.create!(:name => "Mast")
part.mark_for_destruction
ShipPart.find(part.id).update_attribute(:name, 'Deck')
ship.parts.send(:load_target)
assert_equal 'Deck', ship.parts[0].name
end
def test_include_with_order_works def test_include_with_order_works
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)} assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)} assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)}

View file

@ -58,6 +58,19 @@ class CalculationsTest < ActiveRecord::TestCase
[1,6,2].each { |firm_id| assert c.keys.include?(firm_id) } [1,6,2].each { |firm_id| assert c.keys.include?(firm_id) }
end end
def test_should_group_by_multiple_fields
c = Account.count(:all, :group => ['firm_id', :credit_limit])
[ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) }
end
def test_should_group_by_multiple_fields_having_functions
c = Topic.count(:all, :group => [:author_name, 'COALESCE(type, title)'])
assert_equal 1, c[["Nick", "The Third Topic of the day"]]
assert_equal 1, c[["Mary", "Reply"]]
assert_equal 1, c[["David", "The First Topic"]]
assert_equal 1, c[["Carl", "Reply"]]
end
def test_should_group_by_summed_field def test_should_group_by_summed_field
c = Account.sum(:credit_limit, :group => :firm_id) c = Account.sum(:credit_limit, :group => :firm_id)
assert_equal 50, c[1] assert_equal 50, c[1]

View file

@ -809,12 +809,6 @@ class TestHasManyAutosaveAssoictaionWhichItselfHasAutosaveAssociations < ActiveR
@trinket = @part.trinkets.create!(:name => "Necklace") @trinket = @part.trinkets.create!(:name => "Necklace")
end end
test "if association is not loaded and association record is saved and then in memory record attributes should be saved" do
@ship.parts_attributes=[{:id => @part.id,:name =>'Deck'}]
assert_equal 1, @ship.parts.proxy_target.size
assert_equal 'Deck', @ship.parts[0].name
end
test "when grandchild changed in memory, saving parent should save grandchild" do test "when grandchild changed in memory, saving parent should save grandchild" do
@trinket.name = "changed" @trinket.name = "changed"
@ship.save @ship.save

View file

@ -12,6 +12,8 @@ class Company < AbstractCompany
has_many :contracts has_many :contracts
has_many :developers, :through => :contracts has_many :developers, :through => :contracts
named_scope :with_oft_in_name, :conditions => "name LIKE '%oft%'"
def arbitrary_method def arbitrary_method
"I am Jack's profound disappointment" "I am Jack's profound disappointment"
end end

View file

@ -1,3 +1,4 @@
*2.3.11 (February 9, 2011)*
*2.3.10 (October 15, 2010)* *2.3.10 (October 15, 2010)*
*2.3.9 (September 4, 2010)* *2.3.9 (September 4, 2010)*
*2.3.8 (May 24, 2010)* *2.3.8 (May 24, 2010)*

View file

@ -66,7 +66,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end end
s.add_dependency('activesupport', '= 2.3.10' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.11' + PKG_BUILD)
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'active_resource' s.autorequire = 'active_resource'

View file

@ -2,7 +2,7 @@ module ActiveResource
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 3 MINOR = 3
TINY = 10 TINY = 11
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1,3 +1,5 @@
*2.3.11 (February 9, 2011)*
*2.3.10 (October 15, 2010)* *2.3.10 (October 15, 2010)*

View file

@ -1,13 +1,7 @@
# A base class with no predefined methods that tries to behave like Builder's
# BlankSlate in Ruby 1.9. In Ruby pre-1.9, this is actually the
# Builder::BlankSlate class.
#
# Ruby 1.9 introduces BasicObject which differs slightly from Builder's
# BlankSlate that has been used so far. ActiveSupport::BasicObject provides a
# barebones base class that emulates Builder::BlankSlate while still relying on
# Ruby 1.9's BasicObject in Ruby 1.9.
module ActiveSupport module ActiveSupport
if defined? ::BasicObject if defined? ::BasicObject
# A class with no predefined methods that behaves similarly to Builder's
# BlankSlate. Used for proxy classes.
class BasicObject < ::BasicObject class BasicObject < ::BasicObject
undef_method :== undef_method :==
undef_method :equal? undef_method :equal?
@ -18,7 +12,10 @@ module ActiveSupport
end end
end end
else else
require 'blankslate' class BasicObject #:nodoc:
BasicObject = BlankSlate instance_methods.each do |m|
undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$)/
end
end
end end
end end

View file

@ -1,3 +1,5 @@
require 'thread'
module ActiveSupport module ActiveSupport
# Inspired by the buffered logger idea by Ezra # Inspired by the buffered logger idea by Ezra
class BufferedLogger class BufferedLogger

View file

@ -15,7 +15,10 @@ en:
month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December]
abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
# Used in date_select and datime_select. # Used in date_select and datime_select.
order: [ :year, :month, :day ] order:
- :year
- :month
- :day
time: time:
formats: formats:

View file

@ -2,7 +2,7 @@ module ActiveSupport
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 3 MINOR = 3
TINY = 10 TINY = 11
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1,4 +1,5 @@
require 'abstract_unit' require 'abstract_unit'
require 'thread'
class SynchronizationTest < Test::Unit::TestCase class SynchronizationTest < Test::Unit::TestCase
def setup def setup

View file

@ -1,3 +1,5 @@
*2.3.11 (February 9, 2011)*
*2.3.10 (October 15, 2010)* *2.3.10 (October 15, 2010)*
*2.3.9 (September 4, 2010)* *2.3.9 (September 4, 2010)*

View file

@ -313,11 +313,11 @@ spec = Gem::Specification.new do |s|
EOF EOF
s.add_dependency('rake', '>= 0.8.3') s.add_dependency('rake', '>= 0.8.3')
s.add_dependency('activesupport', '= 2.3.10' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.11' + PKG_BUILD)
s.add_dependency('activerecord', '= 2.3.10' + PKG_BUILD) s.add_dependency('activerecord', '= 2.3.11' + PKG_BUILD)
s.add_dependency('actionpack', '= 2.3.10' + PKG_BUILD) s.add_dependency('actionpack', '= 2.3.11' + PKG_BUILD)
s.add_dependency('actionmailer', '= 2.3.10' + PKG_BUILD) s.add_dependency('actionmailer', '= 2.3.11' + PKG_BUILD)
s.add_dependency('activeresource', '= 2.3.10' + PKG_BUILD) s.add_dependency('activeresource', '= 2.3.11' + PKG_BUILD)
s.rdoc_options << '--exclude' << '.' s.rdoc_options << '--exclude' << '.'
s.has_rdoc = false s.has_rdoc = false

View file

@ -218,7 +218,9 @@ If you set a default +:host+ for your mailers you need to pass +:only_path => fa
h4. Sending Multipart Emails h4. Sending Multipart Emails
Action Mailer will automatically send multipart emails if you have different templates for the same action. So, for our UserMailer example, if you have +welcome_email.text.plain.erb+ and +welcome_email.text.html.erb+ in +app/views/user_mailer+, Action Mailer will automatically send a multipart email with the HTML and text versions setup as different parts. Action Mailer will automatically send multipart emails if you have different templates for the same action. So, for our UserMailer example, if you have +welcome_email.text.plain.erb+ and +welcome_email.text.html.erb+ in +app/views/user_mailer+, Action Mailer will automatically send a "multipart/alternative" email with the HTML and text versions setup as different parts.
A "multipart/alternative" content type tells your email client that there are several representations of the same content available and that the email client is free to choose any one to display to the user. In this case we are giving a plain text and HTML version of the same message. But this could also be a text version, and a recording of someone speaking the the same message. It is important to use "multipart/alternative" only when each part has the same content.
To explicitly specify multipart messages, you can do something like: To explicitly specify multipart messages, you can do something like:
@ -242,7 +244,11 @@ end
h4. Sending Emails with Attachments h4. Sending Emails with Attachments
Attachments can be added by using the +attachment+ method: Attachments can be added by using the +attachment+ method. The +attachment+ method has two variations, you can either pass the body in as an option, or create it within a block.
Usually you will use the variation shown below for the "image/jpeg" attachment, here you just pass in the content type and body as a options hash to the attachment method. However, if you need to do some processing to create the attachment, such as with the PDF below, then the block variation can be used.
This email uses the "multipart/mixed" content type because each part is a different block of content. This indicates to the email client that it must show all the parts that it can display to the end user.
<ruby> <ruby>
class UserMailer < ActionMailer::Base class UserMailer < ActionMailer::Base
@ -250,13 +256,14 @@ class UserMailer < ActionMailer::Base
recipients user.email_address recipients user.email_address
subject "New account information" subject "New account information"
from "system@example.com" from "system@example.com"
content_type "multipart/alternative" content_type "multipart/mixed"
attachment :content_type => "image/jpeg", attachment :content_type => "image/jpeg",
:body => File.read("an-image.jpg") :body => File.read("an-image.jpg")
attachment "application/pdf" do |a| attachment "application/pdf" do |a|
a.body = generate_your_pdf_here() pdf = generate_your_pdf_here(:name => user)
a.body = pdf
end end
end end
end end
@ -266,7 +273,11 @@ h4. Sending Multipart Emails with Attachments
Once you use the +attachment+ method, ActionMailer will no longer automagically use the correct template based on the filename. You must declare which template you are using for each content type via the +part+ method. Once you use the +attachment+ method, ActionMailer will no longer automagically use the correct template based on the filename. You must declare which template you are using for each content type via the +part+ method.
In the following example, there would be two template files, +welcome_email_html.erb+ and +welcome_email_plain.erb+ in the +app/views/user_mailer+ folder. Here we are making the email "multipart/mixed" with three top level parts, a "multipart/alternative", an "image/jpeg" and an "application/pdf". Within the "multipart/alternative" we are nesting a "text/html" and "text/plain" version of the same message.
This tells the email client that each of the top level parts should be shown to the end user, however, the first part has the content type "multipart/alternative" and provides two versions of the same message, a plain text and HTML version.
In the following example, there would be two template files, +welcome_email.text.html.erb+ and +welcome_email.text.plain.erb+ in the +app/views/user_mailer+ folder.
<ruby> <ruby>
class UserMailer < ActionMailer::Base class UserMailer < ActionMailer::Base
@ -274,22 +285,28 @@ class UserMailer < ActionMailer::Base
recipients user.email_address recipients user.email_address
subject "New account information" subject "New account information"
from "system@example.com" from "system@example.com"
content_type "multipart/alternative" content_type "multipart/mixed"
part "text/html" do |p| part "multipart/alternative" do |alternative|
p.body = render_message("welcome_email_html", :message => "<h1>HTML content</h1>")
end alternative.part "text/html" do |html|
html.body = render_message("welcome_email.text.html", :message => "<h1>HTML content</h1>")
end
alternative.part "text/plain" do |plain|
plain.body = render_message("welcome_email.text.plain", :message => "text content")
end
part "text/plain" do |p|
p.body = render_message("welcome_email_plain", :message => "text content")
end end
attachment :content_type => "image/jpeg", attachment :content_type => "image/jpeg",
:body => File.read("an-image.jpg") :body => File.read("an-image.jpg")
attachment "application/pdf" do |a| attachment "application/pdf" do |a|
a.body = generate_your_pdf_here() pdf = generate_your_pdf_here(:name => user)
a.body = pdf
end end
end end
end end
</ruby> </ruby>

View file

@ -2,7 +2,7 @@ module Rails
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 3 MINOR = 3
TINY = 10 TINY = 11
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end