Rails 2.3.11, S5 Editing bug.
Upgrade to Rails 2.3.11. Fix a bug where the SVG-Edit button would not appear when editing S5 slideshows.
This commit is contained in:
parent
3843fa608d
commit
844ce0ed40
51 changed files with 444 additions and 299 deletions
|
@ -18,7 +18,7 @@ class Itex
|
|||
begin
|
||||
require 'nokogiri'
|
||||
def self.xmlparse(text)
|
||||
Nokogiri::XML(text) { |config| config.options = Nokogiri::XML::ParseOptions::STRICT }
|
||||
Nokogiri::XML(text) { |config| config.strict }
|
||||
end
|
||||
rescue LoadError
|
||||
require 'rexml/document'
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<%= @web && @web.additional_style ? @web.additional_style.html_safe : '' %>
|
||||
/*]]>*/--></style>
|
||||
<%= javascript_include_tag :defaults %>
|
||||
<%= csrf_meta_tag %>
|
||||
<%- if @web -%>
|
||||
<%- if @web.markup == :markdownMML -%>
|
||||
<script type="text/javascript">
|
||||
|
|
|
@ -69,9 +69,11 @@ function cleanAuthorName() {
|
|||
}
|
||||
|
||||
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] %>');
|
||||
<%- unless @page.categories.include?('S5-slideshow') -%>
|
||||
addS5button('<%= @page.name.escapeHTML %>');
|
||||
<%- end -%>
|
||||
<%- end -%>
|
||||
//--><!]]>
|
||||
</script>
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
1
vendor/rails/actionmailer/CHANGELOG
vendored
1
vendor/rails/actionmailer/CHANGELOG
vendored
|
@ -1,3 +1,4 @@
|
|||
*2.3.11 (February 9, 2011)*
|
||||
*2.3.10 (October 15, 2010)*
|
||||
*2.3.9 (September 4, 2010)*
|
||||
*2.3.8 (May 24, 2010)*
|
||||
|
|
2
vendor/rails/actionmailer/Rakefile
vendored
2
vendor/rails/actionmailer/Rakefile
vendored
|
@ -54,7 +54,7 @@ spec = Gem::Specification.new do |s|
|
|||
s.rubyforge_project = "actionmailer"
|
||||
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.requirements << 'none'
|
||||
|
|
|
@ -195,6 +195,39 @@ module ActionMailer #:nodoc:
|
|||
# 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
|
||||
#
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActionMailer
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 10
|
||||
TINY = 11
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
4
vendor/rails/actionpack/CHANGELOG
vendored
4
vendor/rails/actionpack/CHANGELOG
vendored
|
@ -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.9 (September 4, 2010)*
|
||||
|
|
2
vendor/rails/actionpack/Rakefile
vendored
2
vendor/rails/actionpack/Rakefile
vendored
|
@ -79,7 +79,7 @@ spec = Gem::Specification.new do |s|
|
|||
s.has_rdoc = true
|
||||
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.require_path = 'lib'
|
||||
|
|
|
@ -60,7 +60,7 @@ module ActionController #:nodoc:
|
|||
attr_reader :controller
|
||||
|
||||
def initialize(controller)
|
||||
@controller, @cookies = controller, controller.request.cookies
|
||||
@controller, @cookies, @secure = controller, controller.request.cookies, controller.request.ssl?
|
||||
super()
|
||||
update(@cookies)
|
||||
end
|
||||
|
@ -81,7 +81,7 @@ module ActionController #:nodoc:
|
|||
|
||||
options[:path] = "/" unless options.has_key?(:path)
|
||||
super(key.to_s, options[:value])
|
||||
@controller.response.set_cookie(key, options)
|
||||
@controller.response.set_cookie(key, options) if write_cookie?(options)
|
||||
end
|
||||
|
||||
# Removes the cookie on the client machine by setting the value to an empty string
|
||||
|
@ -126,6 +126,12 @@ module ActionController #:nodoc:
|
|||
def signed
|
||||
@signed ||= SignedCookieJar.new(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write_cookie?(cookie)
|
||||
@secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development?
|
||||
end
|
||||
end
|
||||
|
||||
class PermanentCookieJar < CookieJar #:nodoc:
|
||||
|
|
|
@ -76,7 +76,11 @@ module ActionController #:nodoc:
|
|||
protected
|
||||
# The actual before_filter that is used. Modify this to change how you handle unverified requests.
|
||||
def verify_authenticity_token
|
||||
verified_request? || raise(ActionController::InvalidAuthenticityToken)
|
||||
verified_request? || handle_unverified_request
|
||||
end
|
||||
|
||||
def handle_unverified_request
|
||||
reset_session
|
||||
end
|
||||
|
||||
# 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
|
||||
# * 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 == form_authenticity_param
|
||||
!protect_against_forgery? ||
|
||||
request.get? ||
|
||||
form_authenticity_token == form_authenticity_param ||
|
||||
form_authenticity_token == request.headers['X-CSRF-Token']
|
||||
end
|
||||
|
||||
def form_authenticity_param
|
||||
|
|
|
@ -195,22 +195,8 @@ module ActionController
|
|||
request_cookies = env["rack.request.cookie_hash"]
|
||||
|
||||
if (request_cookies.nil? || request_cookies[@key] != sid) || options[:expire_after]
|
||||
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
|
||||
cookie = {:value => sid}
|
||||
Rack::Utils.set_cookie_header!(response[1], @key, cookie.merge(options))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@ module ActionController
|
|||
|
||||
ENV_SESSION_KEY = "rack.session".freeze
|
||||
ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
|
||||
HTTP_SET_COOKIE = "Set-Cookie".freeze
|
||||
|
||||
# Raised when storing more than 4K of session data.
|
||||
class CookieOverflow < StandardError; end
|
||||
|
@ -116,9 +115,7 @@ module ActionController
|
|||
cookie[:expires] = Time.now + options[:expire_after]
|
||||
end
|
||||
|
||||
cookie = build_cookie(@key, cookie.merge(options))
|
||||
headers[HTTP_SET_COOKIE] = [] if headers[HTTP_SET_COOKIE].blank?
|
||||
headers[HTTP_SET_COOKIE] << cookie
|
||||
Rack::Utils.set_cookie_header!(headers, @key, cookie.merge(options))
|
||||
end
|
||||
|
||||
[status, headers, body]
|
||||
|
@ -130,26 +127,6 @@ module ActionController
|
|||
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options)
|
||||
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)
|
||||
data = unpacked_cookie_data(env)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
begin
|
||||
require_library_or_gem 'memcache'
|
||||
|
||||
require 'thread'
|
||||
module ActionController
|
||||
module Session
|
||||
class MemCacheStore < AbstractStore
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActionPack #:nodoc:
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 10
|
||||
TINY = 11
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ module ActionView #:nodoc:
|
|||
autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper'
|
||||
autoload :CacheHelper, 'action_view/helpers/cache_helper'
|
||||
autoload :CaptureHelper, 'action_view/helpers/capture_helper'
|
||||
autoload :CsrfHelper, 'action_view/helpers/csrf_helper'
|
||||
autoload :DateHelper, 'action_view/helpers/date_helper'
|
||||
autoload :DebugHelper, 'action_view/helpers/debug_helper'
|
||||
autoload :FormHelper, 'action_view/helpers/form_helper'
|
||||
|
@ -38,6 +39,7 @@ module ActionView #:nodoc:
|
|||
include BenchmarkHelper
|
||||
include CacheHelper
|
||||
include CaptureHelper
|
||||
include CsrfHelper
|
||||
include DateHelper
|
||||
include DebugHelper
|
||||
include FormHelper
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'cgi'
|
||||
require 'action_view/helpers/url_helper'
|
||||
require 'action_view/helpers/tag_helper'
|
||||
require 'thread'
|
||||
|
||||
module ActionView
|
||||
module Helpers #:nodoc:
|
||||
|
|
14
vendor/rails/actionpack/lib/action_view/helpers/csrf_helper.rb
vendored
Normal file
14
vendor/rails/actionpack/lib/action_view/helpers/csrf_helper.rb
vendored
Normal 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
|
|
@ -665,7 +665,7 @@ module ActionView
|
|||
#
|
||||
# The HTML specification says unchecked check boxes are not successful, and
|
||||
# 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,
|
||||
# any mass-assignment idiom like
|
||||
#
|
||||
|
@ -673,12 +673,15 @@ module ActionView
|
|||
#
|
||||
# wouldn't update the flag.
|
||||
#
|
||||
# To prevent this the helper generates a hidden field with the same name as
|
||||
# the checkbox after the very check box. So, the client either sends only the
|
||||
# hidden field (representing 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 Rails parameters extraction always
|
||||
# gets the first occurrence of any given key, that works in ordinary forms.
|
||||
# To prevent this the helper generates an auxiliary hidden field before
|
||||
# the very check box. The hidden field has the same name and its
|
||||
# attributes mimick an unchecked check box.
|
||||
#
|
||||
# This way, the client either sends only the hidden field (representing
|
||||
# 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
|
||||
# within an array-like parameter, as in
|
||||
|
@ -689,22 +692,26 @@ module ActionView
|
|||
# <% end %>
|
||||
#
|
||||
# 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
|
||||
# # Let's say that @post.validated? is 1:
|
||||
# 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":
|
||||
# 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")
|
||||
# # => <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")
|
||||
InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
|
||||
|
|
|
@ -471,7 +471,8 @@ module ActionView
|
|||
email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot")
|
||||
|
||||
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)
|
||||
end
|
||||
"<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>"
|
||||
|
|
|
@ -27,7 +27,7 @@ module ActionView
|
|||
def render_partial(view, object = nil, local_assigns = {}, as = nil)
|
||||
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}"
|
||||
object =
|
||||
if view.controller.instance_variable_defined?(ivar)
|
||||
|
@ -43,5 +43,11 @@ module ActionView
|
|||
|
||||
render_template(view, local_assigns)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def local_assigns_key?(local_assigns)
|
||||
local_assigns.key?(:object) || local_assigns.key?(variable_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,11 +100,26 @@ class CookieTest < ActionController::TestCase
|
|||
end
|
||||
|
||||
def test_setting_cookie_with_secure
|
||||
@request.env["HTTPS"] = "on"
|
||||
get :authenticate_with_secure
|
||||
assert_equal ["user_name=david; path=/; secure"], @response.headers["Set-Cookie"]
|
||||
assert_equal({"user_name" => "david"}, @response.cookies)
|
||||
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
|
||||
get :set_multiple_cookies
|
||||
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_equal 100, @controller.send(:cookies).signed[:remember_me]
|
||||
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
|
|
@ -1,4 +1,5 @@
|
|||
require 'abstract_unit'
|
||||
require 'thread'
|
||||
|
||||
class ReloaderTests < ActiveSupport::TestCase
|
||||
Reloader = ActionController::Reloader
|
||||
|
|
|
@ -716,6 +716,11 @@ class TestController < ActionController::Base
|
|||
render :partial => "customer"
|
||||
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
|
||||
render :action => "calling_partial_with_layout"
|
||||
end
|
||||
|
@ -1543,6 +1548,13 @@ class RenderTest < ActionController::TestCase
|
|||
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
|
||||
assert_raise(ActionView::MissingTemplate) do
|
||||
get :missing_partial
|
||||
|
|
|
@ -23,6 +23,10 @@ module RequestForgeryProtectionActions
|
|||
render :text => 'pwn'
|
||||
end
|
||||
|
||||
def meta
|
||||
render :inline => "<%= csrf_meta_tag %>"
|
||||
end
|
||||
|
||||
def rescue_action(e) raise e end
|
||||
end
|
||||
|
||||
|
@ -32,6 +36,16 @@ class RequestForgeryProtectionController < ActionController::Base
|
|||
protect_from_forgery :only => :index
|
||||
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
|
||||
self.allow_forgery_protection = false
|
||||
|
||||
|
@ -54,158 +68,92 @@ end
|
|||
# common test methods
|
||||
|
||||
module RequestForgeryProtectionTests
|
||||
def teardown
|
||||
ActionController::Base.request_forgery_protection_token = nil
|
||||
def setup
|
||||
@token = "cf50faa3fe97702ca1ae"
|
||||
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
|
||||
ActionController::Base.request_forgery_protection_token = :authenticity_token
|
||||
end
|
||||
|
||||
|
||||
|
||||
def test_should_render_form_with_token_tag
|
||||
get :index
|
||||
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
|
||||
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'
|
||||
assert_not_blocked do
|
||||
get :index
|
||||
end
|
||||
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_put_without_token
|
||||
assert_nothing_raised do
|
||||
put :index, :format => 'xml'
|
||||
def test_should_render_button_to_with_token_tag
|
||||
assert_not_blocked do
|
||||
get :show_button
|
||||
end
|
||||
assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token
|
||||
end
|
||||
|
||||
def test_should_allow_api_formatted_delete_without_token
|
||||
assert_nothing_raised do
|
||||
delete :index, :format => 'xml'
|
||||
end
|
||||
def test_should_allow_get
|
||||
assert_not_blocked { get :index }
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_post_sent_as_url_encoded_form_without_token
|
||||
assert_raise(ActionController::InvalidAuthenticityToken) do
|
||||
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
|
||||
post :index, :format => 'xml'
|
||||
end
|
||||
def test_should_allow_post_without_token_on_unsafe_action
|
||||
assert_not_blocked { post :unsafe }
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_put_sent_as_url_encoded_form_without_token
|
||||
assert_raise(ActionController::InvalidAuthenticityToken) do
|
||||
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
|
||||
put :index, :format => 'xml'
|
||||
end
|
||||
def test_should_not_allow_post_without_token
|
||||
assert_blocked { post :index }
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_delete_sent_as_url_encoded_form_without_token
|
||||
assert_raise(ActionController::InvalidAuthenticityToken) do
|
||||
@request.env['CONTENT_TYPE'] = Mime::URL_ENCODED_FORM.to_s
|
||||
delete :index, :format => 'xml'
|
||||
end
|
||||
def test_should_not_allow_post_without_token_irrespective_of_format
|
||||
assert_blocked { post :index, :format=>'xml' }
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_post_sent_as_multipart_form_without_token
|
||||
assert_raise(ActionController::InvalidAuthenticityToken) do
|
||||
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
|
||||
post :index, :format => 'xml'
|
||||
end
|
||||
def test_should_not_allow_put_without_token
|
||||
assert_blocked { put :index }
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_put_sent_as_multipart_form_without_token
|
||||
assert_raise(ActionController::InvalidAuthenticityToken) do
|
||||
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
|
||||
put :index, :format => 'xml'
|
||||
end
|
||||
def test_should_not_allow_delete_without_token
|
||||
assert_blocked { delete :index }
|
||||
end
|
||||
|
||||
def test_should_not_allow_api_formatted_delete_sent_as_multipart_form_without_token
|
||||
assert_raise(ActionController::InvalidAuthenticityToken) do
|
||||
@request.env['CONTENT_TYPE'] = Mime::MULTIPART_FORM.to_s
|
||||
delete :index, :format => 'xml'
|
||||
end
|
||||
def test_should_not_allow_xhr_post_without_token
|
||||
assert_blocked { xhr :post, :index }
|
||||
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
|
||||
|
||||
|
||||
def test_should_allow_post_with_token
|
||||
post :index, :authenticity_token => @token
|
||||
assert_response :success
|
||||
assert_not_blocked { post :index, :authenticity_token => @token }
|
||||
end
|
||||
|
||||
def test_should_allow_put_with_token
|
||||
put :index, :authenticity_token => @token
|
||||
assert_response :success
|
||||
assert_not_blocked { put :index, :authenticity_token => @token }
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def test_should_allow_post_with_xml
|
||||
@request.env['CONTENT_TYPE'] = Mime::XML.to_s
|
||||
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'
|
||||
def assert_not_blocked
|
||||
assert_nothing_raised { yield }
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
|
@ -214,15 +162,20 @@ end
|
|||
|
||||
class RequestForgeryProtectionControllerTest < ActionController::TestCase
|
||||
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)
|
||||
ActionController::Base.request_forgery_protection_token = :authenticity_token
|
||||
test 'should emit a csrf-token meta tag' do
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||
get :meta
|
||||
assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/>), @response.body
|
||||
end
|
||||
end
|
||||
|
||||
class RequestForgeryProtectionControllerUsingOldBehaviourTest < ActionController::TestCase
|
||||
include RequestForgeryProtectionTests
|
||||
def assert_blocked
|
||||
assert_raises(ActionController::InvalidAuthenticityToken) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -251,15 +204,30 @@ class FreeCookieControllerTest < ActionController::TestCase
|
|||
assert_nothing_raised { send(method, :index)}
|
||||
end
|
||||
end
|
||||
|
||||
test 'should not emit a csrf-token meta tag' do
|
||||
get :meta
|
||||
assert_blank @response.body
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CustomAuthenticityParamControllerTest < ActionController::TestCase
|
||||
def setup
|
||||
ActionController::Base.request_forgery_protection_token = :custom_token_name
|
||||
super
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActionController::Base.request_forgery_protection_token = :authenticity_token
|
||||
super
|
||||
end
|
||||
|
||||
def test_should_allow_custom_token
|
||||
post :index, :authenticity_token => 'foobar'
|
||||
post :index, :custom_token_name => 'foobar'
|
||||
assert_response :ok
|
||||
end
|
||||
end
|
||||
|
|
|
@ -106,7 +106,7 @@ class CookieStoreTest < ActionController::IntegrationTest
|
|||
with_test_route_set do
|
||||
get '/set_session_value'
|
||||
assert_response :success
|
||||
assert_equal ["_myapp_session=#{response.body}; path=/; HttpOnly"],
|
||||
assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
|
||||
headers['Set-Cookie']
|
||||
end
|
||||
end
|
||||
|
@ -159,7 +159,7 @@ class CookieStoreTest < ActionController::IntegrationTest
|
|||
with_test_route_set(:secure => true) do
|
||||
get '/set_session_value', nil, 'HTTPS' => 'on'
|
||||
assert_response :success
|
||||
assert_equal ["_myapp_session=#{response.body}; path=/; secure; HttpOnly"],
|
||||
assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly",
|
||||
headers['Set-Cookie']
|
||||
end
|
||||
end
|
||||
|
@ -195,12 +195,12 @@ class CookieStoreTest < ActionController::IntegrationTest
|
|||
get '/set_session_value'
|
||||
assert_response :success
|
||||
session_payload = response.body
|
||||
assert_equal ["_myapp_session=#{response.body}; path=/; HttpOnly"],
|
||||
assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
|
||||
headers['Set-Cookie']
|
||||
|
||||
get '/call_reset_session'
|
||||
assert_response :success
|
||||
assert_not_equal [], headers['Set-Cookie']
|
||||
assert_not_equal "", headers['Set-Cookie']
|
||||
assert_not_equal session_payload, cookies[SessionKey]
|
||||
|
||||
get '/get_session_value'
|
||||
|
@ -214,7 +214,7 @@ class CookieStoreTest < ActionController::IntegrationTest
|
|||
get '/set_session_value'
|
||||
assert_response :success
|
||||
session_payload = response.body
|
||||
assert_equal ["_myapp_session=#{response.body}; path=/; HttpOnly"],
|
||||
assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly",
|
||||
headers['Set-Cookie']
|
||||
|
||||
get '/call_session_clear'
|
||||
|
|
|
@ -333,11 +333,11 @@ class UrlHelperTest < ActionView::TestCase
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def test_mail_with_options
|
||||
|
@ -361,8 +361,8 @@ class UrlHelperTest < ActionView::TestCase
|
|||
assert_dom_equal "<a href=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">me(at)domain.com</a>", mail_to("me@domain.com", nil, :encode => "hex", :replace_at => "(at)")
|
||||
assert_dom_equal "<a href=\"mailto:%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=\"mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d\">me(at)domain(dot)com</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%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%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%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
|
||||
|
||||
def protect_against_forgery?
|
||||
|
|
2
vendor/rails/activerecord/CHANGELOG
vendored
2
vendor/rails/activerecord/CHANGELOG
vendored
|
@ -1,3 +1,5 @@
|
|||
*2.3.11 (February 9, 2011)*
|
||||
|
||||
*2.3.10 (October 15, 2010)*
|
||||
|
||||
* Security Release to fix CVE-2010-3933
|
||||
|
|
2
vendor/rails/activerecord/Rakefile
vendored
2
vendor/rails/activerecord/Rakefile
vendored
|
@ -192,7 +192,7 @@ spec = Gem::Specification.new do |s|
|
|||
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
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_2.sqlite"
|
||||
|
|
|
@ -353,15 +353,14 @@ module ActiveRecord
|
|||
if @target.is_a?(Array) && @target.any?
|
||||
@target = find_target.map do |f|
|
||||
i = @target.index(f)
|
||||
if i
|
||||
@target.delete_at(i).tap do |t|
|
||||
keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
|
||||
t.attributes = f.attributes.except(*keys)
|
||||
end
|
||||
t = @target.delete_at(i) if i
|
||||
if t && t.changed?
|
||||
t
|
||||
else
|
||||
f.mark_for_destruction if t && t.marked_for_destruction?
|
||||
f
|
||||
end
|
||||
end + @target
|
||||
end + @target.find_all {|t| t.new_record?}
|
||||
else
|
||||
@target = find_target
|
||||
end
|
||||
|
@ -375,16 +374,17 @@ module ActiveRecord
|
|||
target
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
def method_missing(method, *args, &block)
|
||||
case method.to_s
|
||||
when 'find_or_create'
|
||||
return find(:first, :conditions => args.first) || create(args.first)
|
||||
when /^find_or_create_by_(.*)$/
|
||||
rest = $1
|
||||
return send("find_by_#{rest}", *args) ||
|
||||
method_missing("create_by_#{rest}", *args)
|
||||
find_args = pull_finder_args_from(DynamicFinderMatch.match(method).attribute_names, *args)
|
||||
return send("find_by_#{rest}", find_args) ||
|
||||
method_missing("create_by_#{rest}", *args, &block)
|
||||
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
|
||||
|
||||
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
|
||||
|
@ -434,20 +434,32 @@ module ActiveRecord
|
|||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
index = @target.index(record)
|
||||
unless @reflection.options[:uniq] && index
|
||||
if index
|
||||
@target[index] = record
|
||||
else
|
||||
@target << record
|
||||
end
|
||||
end
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
set_inverse_instance(record, @owner)
|
||||
record
|
||||
end
|
||||
|
||||
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)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
ensure_owner_is_not_new
|
||||
|
|
|
@ -187,7 +187,7 @@ module ActiveRecord
|
|||
# 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 << ", #{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]
|
||||
sql << " FROM #{options[:from]} "
|
||||
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])
|
||||
|
||||
if options[:group]
|
||||
group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
|
||||
sql << " GROUP BY #{options[group_key]} "
|
||||
group_key = connection.adapter_name == 'FrontBase' ? :group_aliases : :group_fields
|
||||
sql << " GROUP BY #{options[group_key].join(',')} "
|
||||
end
|
||||
|
||||
if options[:group] && options[:having]
|
||||
|
@ -239,24 +239,31 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def execute_grouped_calculation(operation, column_name, column, options) #:nodoc:
|
||||
group_attr = options[:group].to_s
|
||||
association = reflect_on_association(group_attr.to_sym)
|
||||
associated = association && association.macro == :belongs_to # only count belongs_to associations
|
||||
group_field = associated ? association.primary_key_name : group_attr
|
||||
group_alias = column_alias_for(group_field)
|
||||
group_column = column_for group_field
|
||||
sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias))
|
||||
group_attr = options[:group]
|
||||
association = reflect_on_association(group_attr.to_s.to_sym)
|
||||
associated = association && association.macro == :belongs_to # only count belongs_to associations
|
||||
group_fields = Array(associated ? association.primary_key_name : group_attr)
|
||||
group_aliases = []
|
||||
group_columns = {}
|
||||
|
||||
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)
|
||||
aggregate_alias = column_alias_for(operation, column_name)
|
||||
|
||||
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 = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
|
||||
end
|
||||
|
||||
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
|
||||
value = row[aggregate_alias]
|
||||
all[key] = type_cast_calculated_value(value, column, operation)
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveRecord
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 10
|
||||
TINY = 11
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
|
@ -65,6 +65,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal person, person.readers.first.person
|
||||
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
|
||||
person = Person.create! :first_name => 'tenderlove'
|
||||
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"
|
||||
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
|
||||
firm = companies(:first_firm)
|
||||
assert_equal 2, firm.clients.size
|
||||
|
|
|
@ -18,8 +18,6 @@ require 'models/person'
|
|||
require 'models/reader'
|
||||
require 'models/parrot'
|
||||
require 'models/pirate'
|
||||
require 'models/ship'
|
||||
require 'models/ship_part'
|
||||
require 'models/treasure'
|
||||
require 'models/price_estimate'
|
||||
require 'models/club'
|
||||
|
@ -31,23 +29,6 @@ class AssociationsTest < ActiveRecord::TestCase
|
|||
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
|
||||
: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
|
||||
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
|
||||
assert_nothing_raised {Account.find(:first, :order => :id, :include => :firm)}
|
||||
|
|
|
@ -58,6 +58,19 @@ class CalculationsTest < ActiveRecord::TestCase
|
|||
[1,6,2].each { |firm_id| assert c.keys.include?(firm_id) }
|
||||
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
|
||||
c = Account.sum(:credit_limit, :group => :firm_id)
|
||||
assert_equal 50, c[1]
|
||||
|
|
|
@ -808,13 +808,7 @@ class TestHasManyAutosaveAssoictaionWhichItselfHasAutosaveAssociations < ActiveR
|
|||
@part = @ship.parts.create!(:name => "Mast")
|
||||
@trinket = @part.trinkets.create!(:name => "Necklace")
|
||||
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
|
||||
@trinket.name = "changed"
|
||||
@ship.save
|
||||
|
|
|
@ -12,6 +12,8 @@ class Company < AbstractCompany
|
|||
has_many :contracts
|
||||
has_many :developers, :through => :contracts
|
||||
|
||||
named_scope :with_oft_in_name, :conditions => "name LIKE '%oft%'"
|
||||
|
||||
def arbitrary_method
|
||||
"I am Jack's profound disappointment"
|
||||
end
|
||||
|
|
1
vendor/rails/activeresource/CHANGELOG
vendored
1
vendor/rails/activeresource/CHANGELOG
vendored
|
@ -1,3 +1,4 @@
|
|||
*2.3.11 (February 9, 2011)*
|
||||
*2.3.10 (October 15, 2010)*
|
||||
*2.3.9 (September 4, 2010)*
|
||||
*2.3.8 (May 24, 2010)*
|
||||
|
|
2
vendor/rails/activeresource/Rakefile
vendored
2
vendor/rails/activeresource/Rakefile
vendored
|
@ -66,7 +66,7 @@ spec = Gem::Specification.new do |s|
|
|||
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
|
||||
s.add_dependency('activesupport', '= 2.3.10' + PKG_BUILD)
|
||||
s.add_dependency('activesupport', '= 2.3.11' + PKG_BUILD)
|
||||
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'active_resource'
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveResource
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 10
|
||||
TINY = 11
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
2
vendor/rails/activesupport/CHANGELOG
vendored
2
vendor/rails/activesupport/CHANGELOG
vendored
|
@ -1,3 +1,5 @@
|
|||
*2.3.11 (February 9, 2011)*
|
||||
|
||||
*2.3.10 (October 15, 2010)*
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
if defined? ::BasicObject
|
||||
# A class with no predefined methods that behaves similarly to Builder's
|
||||
# BlankSlate. Used for proxy classes.
|
||||
class BasicObject < ::BasicObject
|
||||
undef_method :==
|
||||
undef_method :equal?
|
||||
|
@ -18,7 +12,10 @@ module ActiveSupport
|
|||
end
|
||||
end
|
||||
else
|
||||
require 'blankslate'
|
||||
BasicObject = BlankSlate
|
||||
class BasicObject #:nodoc:
|
||||
instance_methods.each do |m|
|
||||
undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$)/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require 'thread'
|
||||
|
||||
module ActiveSupport
|
||||
# Inspired by the buffered logger idea by Ezra
|
||||
class BufferedLogger
|
||||
|
|
|
@ -15,7 +15,10 @@ en:
|
|||
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]
|
||||
# Used in date_select and datime_select.
|
||||
order: [ :year, :month, :day ]
|
||||
order:
|
||||
- :year
|
||||
- :month
|
||||
- :day
|
||||
|
||||
time:
|
||||
formats:
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveSupport
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 10
|
||||
TINY = 11
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require 'abstract_unit'
|
||||
require 'thread'
|
||||
|
||||
class SynchronizationTest < Test::Unit::TestCase
|
||||
def setup
|
||||
|
|
2
vendor/rails/railties/CHANGELOG
vendored
2
vendor/rails/railties/CHANGELOG
vendored
|
@ -1,3 +1,5 @@
|
|||
*2.3.11 (February 9, 2011)*
|
||||
|
||||
*2.3.10 (October 15, 2010)*
|
||||
|
||||
*2.3.9 (September 4, 2010)*
|
||||
|
|
10
vendor/rails/railties/Rakefile
vendored
10
vendor/rails/railties/Rakefile
vendored
|
@ -313,11 +313,11 @@ spec = Gem::Specification.new do |s|
|
|||
EOF
|
||||
|
||||
s.add_dependency('rake', '>= 0.8.3')
|
||||
s.add_dependency('activesupport', '= 2.3.10' + PKG_BUILD)
|
||||
s.add_dependency('activerecord', '= 2.3.10' + PKG_BUILD)
|
||||
s.add_dependency('actionpack', '= 2.3.10' + PKG_BUILD)
|
||||
s.add_dependency('actionmailer', '= 2.3.10' + PKG_BUILD)
|
||||
s.add_dependency('activeresource', '= 2.3.10' + PKG_BUILD)
|
||||
s.add_dependency('activesupport', '= 2.3.11' + PKG_BUILD)
|
||||
s.add_dependency('activerecord', '= 2.3.11' + PKG_BUILD)
|
||||
s.add_dependency('actionpack', '= 2.3.11' + PKG_BUILD)
|
||||
s.add_dependency('actionmailer', '= 2.3.11' + PKG_BUILD)
|
||||
s.add_dependency('activeresource', '= 2.3.11' + PKG_BUILD)
|
||||
|
||||
s.rdoc_options << '--exclude' << '.'
|
||||
s.has_rdoc = false
|
||||
|
|
|
@ -218,7 +218,9 @@ If you set a default +:host+ for your mailers you need to pass +:only_path => fa
|
|||
|
||||
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:
|
||||
|
||||
|
@ -242,7 +244,11 @@ end
|
|||
|
||||
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>
|
||||
class UserMailer < ActionMailer::Base
|
||||
|
@ -250,13 +256,14 @@ class UserMailer < ActionMailer::Base
|
|||
recipients user.email_address
|
||||
subject "New account information"
|
||||
from "system@example.com"
|
||||
content_type "multipart/alternative"
|
||||
content_type "multipart/mixed"
|
||||
|
||||
attachment :content_type => "image/jpeg",
|
||||
:body => File.read("an-image.jpg")
|
||||
|
||||
attachment "application/pdf" do |a|
|
||||
a.body = generate_your_pdf_here()
|
||||
pdf = generate_your_pdf_here(:name => user)
|
||||
a.body = pdf
|
||||
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.
|
||||
|
||||
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>
|
||||
class UserMailer < ActionMailer::Base
|
||||
|
@ -274,22 +285,28 @@ class UserMailer < ActionMailer::Base
|
|||
recipients user.email_address
|
||||
subject "New account information"
|
||||
from "system@example.com"
|
||||
content_type "multipart/alternative"
|
||||
content_type "multipart/mixed"
|
||||
|
||||
part "text/html" do |p|
|
||||
p.body = render_message("welcome_email_html", :message => "<h1>HTML content</h1>")
|
||||
end
|
||||
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
|
||||
|
||||
part "text/plain" do |p|
|
||||
p.body = render_message("welcome_email_plain", :message => "text content")
|
||||
end
|
||||
|
||||
attachment :content_type => "image/jpeg",
|
||||
:body => File.read("an-image.jpg")
|
||||
|
||||
attachment "application/pdf" do |a|
|
||||
a.body = generate_your_pdf_here()
|
||||
pdf = generate_your_pdf_here(:name => user)
|
||||
a.body = pdf
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
</ruby>
|
||||
|
|
2
vendor/rails/railties/lib/rails/version.rb
vendored
2
vendor/rails/railties/lib/rails/version.rb
vendored
|
@ -2,7 +2,7 @@ module Rails
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 10
|
||||
TINY = 11
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue