提交 deb597d6 编写于 作者: P Pratik Naik

Merge commit 'mainstream/master'

......@@ -33,6 +33,7 @@
gem 'rack', '>= 0.9.0'
require 'rack'
require 'action_controller/rack_ext'
module ActionController
# TODO: Review explicit to see if they will automatically be handled by
......@@ -59,16 +60,14 @@ def self.load_all!
autoload :MiddlewareStack, 'action_controller/middleware_stack'
autoload :MimeResponds, 'action_controller/mime_responds'
autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes'
autoload :Request, 'action_controller/request'
autoload :RequestParser, 'action_controller/request_parser'
autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser'
autoload :UploadedStringIO, 'action_controller/uploaded_file'
autoload :UploadedTempfile, 'action_controller/uploaded_file'
autoload :RecordIdentifier, 'action_controller/record_identifier'
autoload :Response, 'action_controller/response'
autoload :Request, 'action_controller/request'
autoload :RequestForgeryProtection, 'action_controller/request_forgery_protection'
autoload :RequestParser, 'action_controller/request_parser'
autoload :Rescue, 'action_controller/rescue'
autoload :Resources, 'action_controller/resources'
autoload :Response, 'action_controller/response'
autoload :RewindableInput, 'action_controller/rewindable_input'
autoload :Routing, 'action_controller/routing'
autoload :SessionManagement, 'action_controller/session_management'
autoload :StatusCodes, 'action_controller/status_codes'
......@@ -76,9 +75,11 @@ def self.load_all!
autoload :TestCase, 'action_controller/test_case'
autoload :TestProcess, 'action_controller/test_process'
autoload :Translation, 'action_controller/translation'
autoload :UploadedStringIO, 'action_controller/uploaded_file'
autoload :UploadedTempfile, 'action_controller/uploaded_file'
autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser'
autoload :UrlRewriter, 'action_controller/url_rewriter'
autoload :UrlWriter, 'action_controller/url_rewriter'
autoload :VerbPiggybacking, 'action_controller/verb_piggybacking'
autoload :Verification, 'action_controller/verification'
module Assertions
......
......@@ -55,31 +55,7 @@ module HttpAuthentication
# end
# end
#
# Simple Digest example. Note the block must return the user's password so the framework
# can appropriately hash it to check the user's credentials. Returning nil will cause authentication to fail.
#
# class PostsController < ApplicationController
# Users = {"dhh" => "secret"}
#
# before_filter :authenticate, :except => [ :index ]
#
# def index
# render :text => "Everyone can see me!"
# end
#
# def edit
# render :text => "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_digest(realm) do |user_name|
# Users[user_name]
# end
# end
# end
#
#
#
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
......@@ -132,10 +108,7 @@ def authorization(request)
end
def decode_credentials(request)
# Properly decode credentials spanning a new-line
auth = authorization(request)
auth.slice!('Basic ')
ActiveSupport::Base64.decode64(auth || '')
ActiveSupport::Base64.decode64(authorization(request).split.last || '')
end
def encode_credentials(user_name, password)
......@@ -147,165 +120,5 @@ def authentication_request(controller, realm)
controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end
end
module Digest
extend self
module ControllerMethods
def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
begin
authenticate_with_http_digest!(realm, &password_procedure)
rescue ActionController::HttpAuthentication::Error => e
msg = e.message
msg = "#{msg} expected '#{e.expected}' was '#{e.was}'" unless e.expected.nil?
raise msg if e.fatal?
request_http_digest_authentication(realm, msg)
end
end
# Authenticate using HTTP Digest, throwing ActionController::HttpAuthentication::Error on failure.
# This allows more detailed analysis of authentication failures
# to be relayed to the client.
def authenticate_with_http_digest!(realm = "Application", &login_procedure)
HttpAuthentication::Digest.authenticate(self, realm, &login_procedure)
end
# Authenticate with HTTP Digest, returns true or false
def authenticate_with_http_digest(realm = "Application", &login_procedure)
HttpAuthentication::Digest.authenticate(self, realm, &login_procedure) rescue false
end
# Render output including the HTTP Digest authentication header
def request_http_digest_authentication(realm = "Application", message = nil)
HttpAuthentication::Digest.authentication_request(self, realm, message)
end
# Add HTTP Digest authentication header to result headers
def http_digest_authentication_header(realm = "Application")
HttpAuthentication::Digest.authentication_header(self, realm)
end
end
# Raises error unless authentictaion succeeds, returns true otherwise
def authenticate(controller, realm, &password_procedure)
raise Error.new(false), "No authorization header found" unless authorization(controller.request)
validate_digest_response(controller, realm, &password_procedure)
true
end
def authorization(request)
request.env['HTTP_AUTHORIZATION'] ||
request.env['X-HTTP_AUTHORIZATION'] ||
request.env['X_HTTP_AUTHORIZATION'] ||
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
end
# Raises error unless the request credentials response value matches the expected value.
def validate_digest_response(controller, realm, &password_procedure)
credentials = decode_credentials(controller.request)
# Check the nonce, opaque and realm.
# Ignore nc, as we have no way to validate the number of times this nonce has been used
validate_nonce(controller.request, credentials[:nonce])
raise Error.new(false, realm, credentials[:realm]), "Realm doesn't match" unless realm == credentials[:realm]
raise Error.new(true, opaque(controller.request), credentials[:opaque]),"Opaque doesn't match" unless opaque(controller.request) == credentials[:opaque]
password = password_procedure.call(credentials[:username])
raise Error.new(false), "No password" if password.nil?
expected = expected_response(controller.request.env['REQUEST_METHOD'], controller.request.url, credentials, password)
raise Error.new(false, expected, credentials[:response]), "Invalid response" unless expected == credentials[:response]
end
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
def expected_response(http_method, uri, credentials, password)
ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase,uri].join(':'))
::Digest::MD5.hexdigest([ha1,credentials[:nonce], credentials[:nc], credentials[:cnonce],credentials[:qop],ha2].join(':'))
end
def encode_credentials(http_method, credentials, password)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password)
"Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
end
def decode_credentials(request)
authorization(request).to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
key, value = pair.split('=', 2)
hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
hash
end
end
def authentication_header(controller, realm)
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(controller.request)}", opaque="#{opaque(controller.request)}")
end
def authentication_request(controller, realm, message = "HTTP Digest: Access denied")
authentication_header(controller, realm)
controller.send! :render, :text => message, :status => :unauthorized
end
# Uses an MD5 digest based on time to generate a value to be used only once.
#
# A server-specified data string which should be uniquely generated each time a 401 response is made.
# It is recommended that this string be base64 or hexadecimal data.
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
#
# The contents of the nonce are implementation dependent.
# The quality of the implementation depends on a good choice.
# A nonce might, for example, be constructed as the base 64 encoding of
#
# => time-stamp H(time-stamp ":" ETag ":" private-key)
#
# where time-stamp is a server-generated time or other non-repeating value,
# ETag is the value of the HTTP ETag header associated with the requested entity,
# and private-key is data known only to the server.
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
# reject the request if it did not match the nonce from that header or
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
# to limit the reuse of the nonce to the same client that originally got it.
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
# Also, IP address spoofing is not that hard.)
#
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
# POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# of this document.
#
# The nonce is opaque to the client.
def nonce(request, time = Time.now)
session_id = request.is_a?(String) ? request : request.session.session_id
t = time.to_i
hashed = [t, session_id]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
Base64.encode64("#{t}:#{digest}").gsub("\n", '')
end
def validate_nonce(request, value)
t = Base64.decode64(value).split(":").first.to_i
raise Error.new(true), "Stale Nonce" if (t - Time.now.to_i).abs > 10 * 60
n = nonce(request, t)
raise Error.new(true, value, n), "Bad Nonce" unless n == value
end
# Opaque based on digest of session_id
def opaque(request)
session_id = request.is_a?(String) ? request : request.session.session_id
@opaque ||= Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '')
end
end
class Error < RuntimeError
attr_accessor :expected, :was
def initialize(fatal = false, expected = nil, was = nil)
@fatal = fatal
@expected = expected
@was = was
end
def fatal?; @fatal; end
end
end
end
......@@ -2,17 +2,6 @@
require 'uri'
require 'active_support/test_case'
# Monkey patch Rack::Lint to support rewind
module Rack
class Lint
class InputWrapper
def rewind
@input.rewind
end
end
end
end
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
......@@ -68,15 +57,6 @@ class Session
# A running counter of the number of requests processed.
attr_accessor :request_count
# Nonce value for Digest Authentication, implicitly set on response with WWW-Authentication
attr_accessor :nonce
# Opaque value for Digest Authentication, implicitly set on response with WWW-Authentication
attr_accessor :opaque
# Opaque value for Authentication, implicitly set on response with WWW-Authentication
attr_accessor :realm
class MultiPartNeededException < Exception
end
......@@ -252,53 +232,6 @@ def xml_http_request(request_method, path, parameters = nil, headers = nil)
end
alias xhr :xml_http_request
def request_with_noauth(http_method, uri, parameters, headers)
process_with_auth http_method, uri, parameters, headers
end
# Performs a request with the given http_method and parameters, including HTTP Basic authorization headers.
# See get() for more details on paramters and headers.
#
# You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_basic, #post_with_basic,
# #put_with_basic, #delete_with_basic, and #head_with_basic.
def request_with_basic(http_method, uri, parameters, headers, user_name, password)
process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password))
end
# Performs a request with the given http_method and parameters, including HTTP Digest authorization headers.
# See get() for more details on paramters and headers.
#
# You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_digest, #post_with_digest,
# #put_with_digest, #delete_with_digest, and #head_with_digest.
def request_with_digest(http_method, uri, parameters, headers, user_name, password)
# Realm, Nonce, and Opaque taken from previoius 401 response
credentials = {
:username => user_name,
:realm => @realm,
:nonce => @nonce,
:qop => "auth",
:nc => "00000001",
:cnonce => "0a4f113b",
:opaque => @opaque,
:uri => uri
}
raise "Digest request without previous 401 response" if @opaque.nil?
process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Digest.encode_credentials(http_method, credentials, password))
end
# def get_with_basic, def post_with_basic, def put_with_basic, def delete_with_basic, def head_with_basic
# def get_with_digest, def post_with_digest, def put_with_digest, def delete_with_digest, def head_with_digest
[:get, :post, :put, :delete, :head].each do |method|
[:noauth, :basic, :digest].each do |auth_type|
define_method("#{method}_with_#{auth_type}") do |uri, parameters, headers, *auth|
send("request_with_#{auth_type}", method, uri, parameters, headers, *auth)
end
end
end
# Returns the URL for the given options, according to the rules specified
# in the application's routes.
def url_for(options)
......@@ -423,32 +356,6 @@ def process(method, path, parameters = nil, headers = nil)
return status
end
# Same as process, but handles authentication returns to perform
# Basic or Digest authentication
def process_with_auth(method, path, parameters = nil, headers = nil)
status = process(method, path, parameters, headers)
if status == 401
# Extract authentication information from response
auth_data = @response.headers['WWW-Authenticate']
if /^Basic /.match(auth_data)
# extract realm, to be used in subsequent request
@realm = auth_header.split(' ')[1]
elsif /^Digest/.match(auth_data)
creds = auth_data.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
key, value = pair.split('=', 2)
hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
hash
end
@realm = creds[:realm]
@nonce = creds[:nonce]
@opaque = creds[:opaque]
end
end
return status
end
# Encode the cookies hash in a format suitable for passing to a
# request.
def encode_cookies
......@@ -513,7 +420,7 @@ def multipart_requestify(params, first=true)
def multipart_body(params, boundary)
multipart_requestify(params).map do |key, value|
if value.respond_to?(:original_filename)
File.open(value.path) do |f|
File.open(value.path, "rb") do |f|
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
<<-EOF
......
......@@ -32,6 +32,8 @@ def klass
else
@klass.to_s.constantize
end
rescue NameError
@klass
end
def active?
......
......@@ -18,4 +18,5 @@
)
end
use ActionController::VerbPiggybacking
use ActionController::RewindableInput
use Rack::MethodOverride
module Rack
module Utils
module Multipart
class << self
def parse_multipart_with_rewind(env)
result = parse_multipart_without_rewind(env)
begin
env['rack.input'].rewind if env['rack.input'].respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
result
end
alias_method_chain :parse_multipart, :rewind
end
end
end
end
module ActionController
class RewindableInput
class RewindableIO < ActiveSupport::BasicObject
def initialize(io)
@io = io
end
def read(*args)
read_original_io
@io.read(*args)
end
def rewind
read_original_io
@io.rewind
end
def string
@string
end
def method_missing(method, *args, &block)
@io.send(method, *args, &block)
end
private
def read_original_io
unless @string
@string = @io.read
@io = StringIO.new(@string)
end
end
end
def initialize(app)
@app = app
end
def call(env)
env['rack.input'] = RewindableIO.new(env['rack.input'])
@app.call(env)
end
end
end
......@@ -163,9 +163,9 @@ def unmarshal(cookie)
def ensure_session_key(key)
if key.blank?
raise ArgumentError, 'A session_key is required to write a ' +
raise ArgumentError, 'A key is required to write a ' +
'cookie containing the session data. Use ' +
'config.action_controller.session = { :session_key => ' +
'config.action_controller.session = { :key => ' +
'"_myapp_session", :secret => "some secret phrase" } in ' +
'config/environment.rb'
end
......@@ -181,7 +181,7 @@ def ensure_secret_secure(secret)
if secret.blank?
raise ArgumentError, "A secret is required to generate an " +
"integrity hash for cookie session data. Use " +
"config.action_controller.session = { :session_key => " +
"config.action_controller.session = { :key => " +
"\"_myapp_session\", :secret => \"some secret phrase of at " +
"least #{SECRET_MIN_LENGTH} characters\" } " +
"in config/environment.rb"
......
......@@ -484,7 +484,8 @@ def method_missing(selector, *args, &block)
#
# post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
def fixture_file_upload(path, mime_type = nil, binary = false)
ActionController::TestUploadedFile.new("#{ActionController::TestCase.try(:fixture_path)}#{path}", mime_type, binary)
fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
end
# A helper to make it easier to test different route configurations.
......
module ActionController
# TODO: Use Rack::MethodOverride when it is released
class VerbPiggybacking
HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
def initialize(app)
@app = app
end
def call(env)
if env["REQUEST_METHOD"] == "POST"
req = Request.new(env)
if method = (req.parameters[:_method] || env["HTTP_X_HTTP_METHOD_OVERRIDE"])
method = method.to_s.upcase
if HTTP_METHODS.include?(method)
env["REQUEST_METHOD"] = method
end
end
end
@app.call(env)
end
end
end
require 'abstract_unit'
class HttpDigestAuthenticationTest < Test::Unit::TestCase
include ActionController::HttpAuthentication::Digest
class DummyController
attr_accessor :headers, :renders, :request, :response
def initialize
@headers, @renders = {}, []
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
request.session.session_id = "test_session"
end
def render(options)
self.renderers << options
end
end
def setup
@controller = DummyController.new
@credentials = {
:username => "dhh",
:realm => "testrealm@host.com",
:nonce => ActionController::HttpAuthentication::Digest.nonce(@controller.request),
:qop => "auth",
:nc => "00000001",
:cnonce => "0a4f113b",
:opaque => ActionController::HttpAuthentication::Digest.opaque(@controller.request),
:uri => "http://test.host/"
}
@encoded_credentials = ActionController::HttpAuthentication::Digest.encode_credentials("GET", @credentials, "secret")
end
def test_decode_credentials
set_headers
assert_equal @credentials, decode_credentials(@controller.request)
end
def test_nonce_format
assert_nothing_thrown do
validate_nonce(@controller.request, nonce(@controller.request))
end
end
def test_authenticate_should_raise_for_nil_password
set_headers ActionController::HttpAuthentication::Digest.encode_credentials(:get, @credentials, nil)
assert_raise ActionController::HttpAuthentication::Error do
authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "secret" }
end
end
def test_authenticate_should_raise_for_incorrect_password
set_headers
assert_raise ActionController::HttpAuthentication::Error do
authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "bad password" }
end
end
def test_authenticate_should_not_raise_for_correct_password
set_headers
assert_nothing_thrown do
authenticate(@controller, @credentials[:realm]) { |user| user == "dhh" && "secret" }
end
end
private
def set_headers(value = @encoded_credentials, name = 'HTTP_AUTHORIZATION', method = "GET")
@controller.request.env[name] = value
@controller.request.env["REQUEST_METHOD"] = method
end
end
......@@ -8,25 +8,7 @@ class SessionTest < Test::Unit::TestCase
}
def setup
@credentials = {
:username => "username",
:realm => "MyApp",
:nonce => ActionController::HttpAuthentication::Digest.nonce("session_id"),
:qop => "auth",
:nc => "00000001",
:cnonce => "0a4f113b",
:opaque => ActionController::HttpAuthentication::Digest.opaque("session_id"),
:uri => "/index"
}
@session = ActionController::Integration::Session.new(StubApp)
@session.nonce = @credentials[:nonce]
@session.opaque = @credentials[:opaque]
@session.realm = @credentials[:realm]
end
def encoded_credentials(method)
ActionController::HttpAuthentication::Digest.encode_credentials(method, @credentials, "password")
end
def test_https_bang_works_and_sets_truth_by_default
......@@ -150,76 +132,6 @@ def test_head
@session.head(path,params,headers)
end
def test_get_with_basic
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
@session.expects(:process).with(:get,path,params,expected_headers)
@session.get_with_basic(path,params,headers,'username','password')
end
def test_post_with_basic
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
@session.expects(:process).with(:post,path,params,expected_headers)
@session.post_with_basic(path,params,headers,'username','password')
end
def test_put_with_basic
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
@session.expects(:process).with(:put,path,params,expected_headers)
@session.put_with_basic(path,params,headers,'username','password')
end
def test_delete_with_basic
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
@session.expects(:process).with(:delete,path,params,expected_headers)
@session.delete_with_basic(path,params,headers,'username','password')
end
def test_head_with_basic
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => "Basic dXNlcm5hbWU6cGFzc3dvcmQ=\n")
@session.expects(:process).with(:head,path,params,expected_headers)
@session.head_with_basic(path,params,headers,'username','password')
end
def test_get_with_digest
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => encoded_credentials(:get))
@session.expects(:process).with(:get,path,params,expected_headers)
@session.get_with_digest(path,params,headers,'username','password')
end
def test_post_with_digest
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => encoded_credentials(:post))
@session.expects(:process).with(:post,path,params,expected_headers)
@session.post_with_digest(path,params,headers,'username','password')
end
def test_put_with_digest
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => encoded_credentials(:put))
@session.expects(:process).with(:put,path,params,expected_headers)
@session.put_with_digest(path,params,headers,'username','password')
end
def test_delete_with_digest
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => encoded_credentials(:delete))
@session.expects(:process).with(:delete,path,params,expected_headers)
@session.delete_with_digest(path,params,headers,'username','password')
end
def test_head_with_digest
path = "/index"; params = "blah"; headers = {:location => 'blah'}
expected_headers = headers.merge(:authorization => encoded_credentials(:head))
@session.expects(:process).with(:head,path,params,expected_headers)
@session.head_with_digest(path,params,headers,'username','password')
end
def test_xml_http_request_get
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
......
require 'abstract_unit'
unless defined? ApplicationController
class ApplicationController < ActionController::Base
end
end
class UploadTestController < ActionController::Base
def update
SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type]
render :text => "got here"
end
def read
render :text => "File: #{params[:uploaded_data].read}"
end
end
class SessionUploadTest < ActionController::IntegrationTest
FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart'
class << self
attr_accessor :last_request_type
end
def test_upload_and_read_file
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
# The lint wrapper is used in integration tests
# instead of a normal StringIO class
InputWrapper = Rack::Lint::InputWrapper
def test_post_with_upload_with_unrewindable_input
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
def test_post_with_upload_with_params_parsing
with_test_routing do
params = { :uploaded_data => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") }
post '/update', params, :location => 'blah'
assert_equal(:multipart_form, SessionUploadTest.last_request_type)
end
end
private
def with_test_routing
with_routing do |set|
set.draw do |map|
map.update 'update', :controller => "upload_test", :action => "update", :method => :post
map.read 'read', :controller => "upload_test", :action => "read", :method => :post
end
yield
end
end
end
require 'abstract_unit'
class MultipartParamsParsingTest < ActionController::IntegrationTest
class TestController < ActionController::Base
class << self
attr_accessor :last_request_parameters
end
def parse
self.class.last_request_parameters = request.request_parameters
head :ok
end
def read
render :text => "File: #{params[:uploaded_data].read}"
end
end
FIXTURE_PATH = File.dirname(__FILE__) + '/../../fixtures/multipart'
def teardown
TestController.last_request_parameters = nil
end
test "parses single parameter" do
assert_equal({ 'foo' => 'bar' }, parse_multipart('single_parameter'))
end
test "parses bracketed parameters" do
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
end
test "parses text file" do
params = parse_multipart('text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'contents', file.read
end
test "parses boundary problem file" do
params = parse_multipart('boundary_problem_file')
assert_equal %w(file foo), params.keys.sort
file = params['file']
foo = params['foo']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'bar', foo
end
test "parses large text file" do
params = parse_multipart('large_text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert ('a' * 20480) == file.read
end
test "parses binary file" do
params = parse_multipart('binary_file')
assert_equal %w(file flowers foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.csv', file.original_filename
assert_nil file.content_type
assert_equal 'contents', file.read
file = params['flowers']
assert_kind_of StringIO, file
assert_equal 'flowers.jpg', file.original_filename
assert_equal "image/jpeg", file.content_type
assert_equal 19512, file.size
end
test "parses mixed files" do
params = parse_multipart('mixed_files')
assert_equal %w(files foo), params.keys.sort
assert_equal 'bar', params['foo']
# Ruby CGI doesn't handle multipart/mixed for us.
files = params['files']
assert_kind_of String, files
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
assert_equal 19756, files.size
end
test "uploads and reads binary file" do
with_test_routing do
fixture = FIXTURE_PATH + "/mona_lisa.jpg"
params = { :uploaded_data => fixture_file_upload(fixture, "image/jpg") }
post '/read', params
expected_length = 'File: '.length + File.size(fixture)
assert_equal expected_length, response.content_length
end
end
test "uploads and reads file" do
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
# The lint wrapper is used in integration tests
# instead of a normal StringIO class
InputWrapper = Rack::Lint::InputWrapper
test "parses unwindable stream" do
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
params = parse_multipart('large_text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
end
test "uploads and reads file with unwindable input" do
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
test "passes through rack middleware and uploads file" do
with_muck_middleware do
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
end
test "passes through rack middleware and uploads file with unwindable input" do
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
with_muck_middleware do
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
end
private
def fixture(name)
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
{ "rack.input" => file.read,
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
"CONTENT_LENGTH" => file.stat.size.to_s }
end
end
def parse_multipart(name)
with_test_routing do
headers = fixture(name)
post "/parse", headers.delete("rack.input"), headers
assert_response :ok
TestController.last_request_parameters
end
end
def with_test_routing
with_routing do |set|
set.draw do |map|
map.connect ':action', :controller => "multipart_params_parsing_test/test"
end
yield
end
end
class MuckMiddleware
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
req.params # Parse params
@app.call(env)
end
end
def with_muck_middleware
original_middleware = ActionController::Dispatcher.middleware
middleware = original_middleware.dup
middleware.insert_after ActionController::RewindableInput, MuckMiddleware
ActionController::Dispatcher.middleware = middleware
yield
ActionController::Dispatcher.middleware = original_middleware
end
end
require 'abstract_unit'
class UrlEncodedParamsParsingTest < ActionController::IntegrationTest
class TestController < ActionController::Base
class << self
attr_accessor :last_request_parameters, :last_request_type
end
def parse
self.class.last_request_parameters = request.request_parameters
head :ok
end
end
def teardown
TestController.last_request_parameters = nil
end
test "parses unbalanced query string with array" do
assert_parses(
{'location' => ["1", "2"], 'age_group' => ["2"]},
"location[]=1&location[]=2&age_group[]=2"
)
end
test "parses nested hash" do
query = [
"note[viewers][viewer][][type]=User",
"note[viewers][viewer][][id]=1",
"note[viewers][viewer][][type]=Group",
"note[viewers][viewer][][id]=2"
].join("&")
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
assert_parses(expected, query)
end
test "parses more complex nesting" do
query = [
"customers[boston][first][name]=David",
"customers[boston][first][url]=http://David",
"customers[boston][second][name]=Allan",
"customers[boston][second][url]=http://Allan",
"something_else=blah",
"something_nil=",
"something_empty=",
"products[first]=Apple Computer",
"products[second]=Pc",
"=Save"
].join("&")
expected = {
"customers" => {
"boston" => {
"first" => {
"name" => "David",
"url" => "http://David"
},
"second" => {
"name" => "Allan",
"url" => "http://Allan"
}
}
},
"something_else" => "blah",
"something_empty" => "",
"something_nil" => "",
"products" => {
"first" => "Apple Computer",
"second" => "Pc"
}
}
assert_parses expected, query
end
test "parses params with array" do
query = "selected[]=1&selected[]=2&selected[]=3"
expected = { "selected" => [ "1", "2", "3" ] }
assert_parses expected, query
end
test "parses params with non alphanumeric name" do
query = "a/b[c]=d"
expected = { "a/b" => { "c" => "d" }}
assert_parses expected, query
end
test "parses params with single brackets in the middle" do
query = "a/b[c]d=e"
expected = { "a/b" => {} }
assert_parses expected, query
end
test "parses params with separated brackets" do
query = "a/b@[c]d[e]=f"
expected = { "a/b@" => { }}
assert_parses expected, query
end
test "parses params with separated brackets and array" do
query = "a/b@[c]d[e][]=f"
expected = { "a/b@" => { }}
assert_parses expected, query
end
test "parses params with unmatched brackets and array" do
query = "a/b@[c][d[e][]=f"
expected = { "a/b@" => { "c" => { }}}
assert_parses expected, query
end
test "parses params with nil key" do
query = "=&test2=value1"
expected = { "test2" => "value1" }
assert_parses expected, query
end
test "parses params with array prefix and hashes" do
query = "a[][b][c]=d"
expected = {"a" => [{"b" => {"c" => "d"}}]}
assert_parses expected, query
end
test "parses params with complex nesting" do
query = "a[][b][c][][d][]=e"
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
assert_parses expected, query
end
test "parses params with file path" do
query = [
"customers[boston][first][name]=David",
"something_else=blah",
"logo=#{File.expand_path(__FILE__)}"
].join("&")
expected = {
"customers" => {
"boston" => {
"first" => {
"name" => "David"
}
}
},
"something_else" => "blah",
"logo" => File.expand_path(__FILE__),
}
assert_parses expected, query
end
test "passes through rack middleware and parses params" do
with_muck_middleware do
assert_parses({ "a" => { "b" => "c" } }, "a[b]=c")
end
end
# The lint wrapper is used in integration tests
# instead of a normal StringIO class
InputWrapper = Rack::Lint::InputWrapper
test "passes through rack middleware and parses params with unwindable input" do
InputWrapper.any_instance.stubs(:rewind).raises(Errno::ESPIPE)
with_muck_middleware do
assert_parses({ "a" => { "b" => "c" } }, "a[b]=c")
end
end
private
class MuckMiddleware
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
req.params # Parse params
@app.call(env)
end
end
def with_muck_middleware
original_middleware = ActionController::Dispatcher.middleware
middleware = original_middleware.dup
middleware.insert_after ActionController::RewindableInput, MuckMiddleware
ActionController::Dispatcher.middleware = middleware
yield
ActionController::Dispatcher.middleware = original_middleware
end
def with_test_routing
with_routing do |set|
set.draw do |map|
map.connect ':action', :controller => "url_encoded_params_parsing_test/test"
end
yield
end
end
def assert_parses(expected, actual)
with_test_routing do
post "/parse", actual
assert_response :ok
assert_equal(expected, TestController.last_request_parameters)
end
end
end
......@@ -405,308 +405,3 @@ def request_method=(method)
@request.request_method(true)
end
end
class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase
def test_unbalanced_query_string_with_array
assert_equal(
{'location' => ["1", "2"], 'age_group' => ["2"]},
ActionController::RequestParser.parse_request_parameters({'location[]' => ["1", "2"], 'age_group[]' => ["2"]})
)
end
def test_request_hash_parsing
query = {
"note[viewers][viewer][][type]" => ["User", "Group"],
"note[viewers][viewer][][id]" => ["1", "2"]
}
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
assert_equal(expected, ActionController::RequestParser.parse_request_parameters(query))
end
def test_parse_params
input = {
"customers[boston][first][name]" => [ "David" ],
"customers[boston][first][url]" => [ "http://David" ],
"customers[boston][second][name]" => [ "Allan" ],
"customers[boston][second][url]" => [ "http://Allan" ],
"something_else" => [ "blah" ],
"something_nil" => [ nil ],
"something_empty" => [ "" ],
"products[first]" => [ "Apple Computer" ],
"products[second]" => [ "Pc" ],
"" => [ 'Save' ]
}
expected_output = {
"customers" => {
"boston" => {
"first" => {
"name" => "David",
"url" => "http://David"
},
"second" => {
"name" => "Allan",
"url" => "http://Allan"
}
}
},
"something_else" => "blah",
"something_empty" => "",
"something_nil" => "",
"products" => {
"first" => "Apple Computer",
"second" => "Pc"
}
}
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
end
UploadedStringIO = ActionController::UploadedStringIO
class MockUpload < UploadedStringIO
def initialize(content_type, original_path, *args)
self.content_type = content_type
self.original_path = original_path
super *args
end
end
def test_parse_params_from_multipart_upload
file = MockUpload.new('img/jpeg', 'foo.jpg')
ie_file = MockUpload.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg')
non_file_text_part = MockUpload.new('text/plain', '', 'abc')
input = {
"something" => [ UploadedStringIO.new("") ],
"array_of_stringios" => [[ UploadedStringIO.new("One"), UploadedStringIO.new("Two") ]],
"mixed_types_array" => [[ UploadedStringIO.new("Three"), "NotStringIO" ]],
"mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", UploadedStringIO.new("StringIO")]],
"ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", UploadedStringIO.new("StringIO")]],
"products[string]" => [ UploadedStringIO.new("Apple Computer") ],
"products[file]" => [ file ],
"ie_products[string]" => [ UploadedStringIO.new("Microsoft") ],
"ie_products[file]" => [ ie_file ],
"text_part" => [non_file_text_part]
}
expected_output = {
"something" => "",
"array_of_stringios" => ["One", "Two"],
"mixed_types_array" => [ "Three", "NotStringIO" ],
"mixed_types_as_checkboxes" => {
"strings" => {
"nested" => [ file, "String", "StringIO" ]
},
},
"ie_mixed_types_as_checkboxes" => {
"strings" => {
"nested" => [ ie_file, "String", "StringIO" ]
},
},
"products" => {
"string" => "Apple Computer",
"file" => file
},
"ie_products" => {
"string" => "Microsoft",
"file" => ie_file
},
"text_part" => "abc"
}
params = ActionController::RequestParser.parse_request_parameters(input)
assert_equal expected_output, params
# Lone filenames are preserved.
assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
assert_equal 'foo.jpg', params['products']['file'].original_filename
# But full Windows paths are reduced to their basename.
assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
assert_equal 'bar.jpg', params['ie_products']['file'].original_filename
end
def test_parse_params_with_file
input = {
"customers[boston][first][name]" => [ "David" ],
"something_else" => [ "blah" ],
"logo" => [ File.new(File.dirname(__FILE__) + "/rack_test.rb").path ]
}
expected_output = {
"customers" => {
"boston" => {
"first" => {
"name" => "David"
}
}
},
"something_else" => "blah",
"logo" => File.new(File.dirname(__FILE__) + "/rack_test.rb").path,
}
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_array
input = { "selected[]" => [ "1", "2", "3" ] }
expected_output = { "selected" => [ "1", "2", "3" ] }
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_non_alphanumeric_name
input = { "a/b[c]" => %w(d) }
expected = { "a/b" => { "c" => "d" }}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_single_brackets_in_middle
input = { "a/b[c]d" => %w(e) }
expected = { "a/b" => {} }
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_separated_brackets
input = { "a/b@[c]d[e]" => %w(f) }
expected = { "a/b@" => { }}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_separated_brackets_and_array
input = { "a/b@[c]d[e][]" => %w(f) }
expected = { "a/b@" => { }}
assert_equal expected , ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_unmatched_brackets_and_array
input = { "a/b@[c][d[e][]" => %w(f) }
expected = { "a/b@" => { "c" => { }}}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_nil_key
input = { nil => nil, "test2" => %w(value1) }
expected = { "test2" => "value1" }
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_array_prefix_and_hashes
input = { "a[][b][c]" => %w(d) }
expected = {"a" => [{"b" => {"c" => "d"}}]}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_complex_nesting
input = { "a[][b][c][][d][]" => %w(e) }
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
end
class MultipartRequestParameterParsingTest < ActiveSupport::TestCase
FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart'
def test_single_parameter
params = parse_multipart('single_parameter')
assert_equal({ 'foo' => 'bar' }, params)
end
def test_bracketed_param
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
end
def test_text_file
params = parse_multipart('text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'contents', file.read
end
def test_boundary_problem_file
params = parse_multipart('boundary_problem_file')
assert_equal %w(file foo), params.keys.sort
file = params['file']
foo = params['foo']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'bar', foo
end
def test_large_text_file
params = parse_multipart('large_text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert ('a' * 20480) == file.read
end
uses_mocha "test_no_rewind_stream" do
def test_no_rewind_stream
# Ensures that parse_multipart_form_parameters works with streams that cannot be rewound
file = File.open(File.join(FIXTURE_PATH, 'large_text_file'), 'rb')
file.expects(:rewind).raises(Errno::ESPIPE)
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
assert_not_equal 0, file.pos # file was not rewound after reading
end
end
def test_binary_file
params = parse_multipart('binary_file')
assert_equal %w(file flowers foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.csv', file.original_filename
assert_nil file.content_type
assert_equal 'contents', file.read
file = params['flowers']
assert_kind_of StringIO, file
assert_equal 'flowers.jpg', file.original_filename
assert_equal "image/jpeg", file.content_type
assert_equal 19512, file.size
#assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read
end
def test_mixed_files
params = parse_multipart('mixed_files')
assert_equal %w(files foo), params.keys.sort
assert_equal 'bar', params['foo']
# Ruby CGI doesn't handle multipart/mixed for us.
files = params['files']
assert_kind_of String, files
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
assert_equal 19756, files.size
end
private
def parse_multipart(name)
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
assert_equal 0, file.pos # file was rewound after reading
params
end
end
end
*2.3.0/3.0*
* Support nested transactions using database savepoints. #383 [Jonathan Viney, Hongli Lai]
* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
......
......@@ -53,36 +53,124 @@ def update(sql, name = nil)
def delete(sql, name = nil)
delete_sql(sql, name)
end
# Checks whether there is currently no transaction active. This is done
# by querying the database driver, and does not use the transaction
# house-keeping information recorded by #increment_open_transactions and
# friends.
#
# Returns true if there is no transaction active, false if there is a
# transaction active, and nil if this information is unknown.
#
# Not all adapters supports transaction state introspection. Currently,
# only the PostgreSQL adapter supports this.
def outside_transaction?
nil
end
# Runs the given block in a database transaction, and returns the result
# of the block.
#
# == Nested transactions support
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that supports true nested transactions that
# we're aware of, is MS-SQL.
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
# of a nested call, #transaction will behave as follows:
#
# - The block will be run without doing anything. All database statements
# that happen within the block are effectively appended to the already
# open database transaction.
# - However, if +:requires_new+ is set, the block will be wrapped in a
# database savepoint acting as a sub-transaction.
#
# === Caveats
#
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
# then any created savepoints will be automatically released. For example,
# if you've created a savepoint, then you execute a CREATE TABLE statement,
# then the savepoint that was created will be automatically released.
#
# This means that, on MySQL, you shouldn't execute DDL operations inside
# a #transaction call that you know might create a savepoint. Otherwise,
# #transaction will raise exceptions when it tries to release the
# already-automatically-released savepoints:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...)
# # active_record_1 now automatically released
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
# end
def transaction(options = {})
options.assert_valid_keys :requires_new, :joinable
last_transaction_joinable = @transaction_joinable
if options.has_key?(:joinable)
@transaction_joinable = options[:joinable]
else
@transaction_joinable = true
end
requires_new = options[:requires_new] || !last_transaction_joinable
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
if requires_new || open_transactions == 0
if open_transactions == 0
begin_db_transaction
elsif requires_new
create_savepoint
end
increment_open_transactions
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open
if transaction_open && !outside_transaction?
transaction_open = false
rollback_db_transaction
decrement_open_transactions
if open_transactions == 0
rollback_db_transaction
else
rollback_to_savepoint
end
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
end
ensure
if transaction_open
@transaction_joinable = last_transaction_joinable
if outside_transaction?
@open_transactions = 0
elsif transaction_open
decrement_open_transactions
begin
commit_db_transaction
if open_transactions == 0
commit_db_transaction
else
release_savepoint
end
rescue Exception => database_transaction_rollback
rollback_db_transaction
if open_transactions == 0
rollback_db_transaction
else
rollback_to_savepoint
end
raise
end
end
end
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
......
......@@ -66,6 +66,12 @@ def supports_count_distinct?
def supports_ddl_transactions?
false
end
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
# does not.
def supports_savepoints?
false
end
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
......@@ -160,6 +166,23 @@ def decrement_open_transactions
@open_transactions -= 1
end
def transaction_joinable=(joinable)
@transaction_joinable = joinable
end
def create_savepoint
end
def rollback_to_savepoint
end
def release_savepoint
end
def current_savepoint_name
"active_record_#{open_transactions}"
end
def log_info(sql, name, ms)
if @logger && @logger.debug?
name = '%s (%.1fms)' % [name || 'SQL', ms]
......
......@@ -210,6 +210,10 @@ def adapter_name #:nodoc:
def supports_migrations? #:nodoc:
true
end
def supports_savepoints? #:nodoc:
true
end
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
......@@ -349,6 +353,17 @@ def rollback_db_transaction #:nodoc:
# Transactions aren't supported
end
def create_savepoint
execute("SAVEPOINT #{current_savepoint_name}")
end
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
......
......@@ -272,6 +272,10 @@ def supports_insert_with_returning?
def supports_ddl_transactions?
true
end
def supports_savepoints?
true
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
......@@ -528,45 +532,26 @@ def commit_db_transaction
def rollback_db_transaction
execute "ROLLBACK"
end
if defined?(PGconn::PQTRANS_IDLE)
# The ruby-pg driver supports inspecting the transaction status,
# while the ruby-postgres driver does not.
def outside_transaction?
@connection.transaction_status == PGconn::PQTRANS_IDLE
end
end
# ruby-pg defines Ruby constants for transaction status,
# ruby-postgres does not.
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
# Check whether a transaction is active.
def transaction_active?
@connection.transaction_status != PQTRANS_IDLE
def create_savepoint
execute("SAVEPOINT #{current_savepoint_name}")
end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open && transaction_active?
transaction_open = false
rollback_db_transaction
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
if transaction_open && transaction_active?
begin
commit_db_transaction
rescue Exception => database_transaction_rollback
rollback_db_transaction
raise
end
end
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# SCHEMA STATEMENTS ========================================
......
......@@ -516,7 +516,7 @@ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
all_loaded_fixtures.update(fixtures_map)
connection.transaction(connection.open_transactions.zero?) do
connection.transaction(:requires_new => true) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
......@@ -937,6 +937,7 @@ def setup_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.transaction_joinable = false
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else
......
......@@ -53,11 +53,6 @@ class Session < ActiveRecord::Base
before_save :raise_on_session_data_overflow!
class << self
# Don't try to reload ARStore::Session in dev mode.
def reloadable? #:nodoc:
false
end
def data_column_size_limit
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
end
......
......@@ -120,16 +120,66 @@ def self.included(base)
# end
#
# One should restart the entire transaction if a StatementError occurred.
#
# == Nested transactions
#
# #transaction calls can be nested. By default, this makes all database
# statements in the nested transaction block become part of the parent
# transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
# User.find(:all) # => empty
#
# It is also possible to requires a sub-transaction by passing
# <tt>:requires_new => true</tt>. If anything goes wrong, the
# database rolls back to the beginning of the sub-transaction
# without rolling back the parent transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction(:requires_new => true) do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
# User.find(:all) # => Returns only Kotori
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints. See
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# for more information about savepoints.
#
# === Caveats
#
# If you're on MySQL, then do not use DDL operations in nested transactions
# blocks that are emulated with savepoints. That is, do not execute statements
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
# releases all savepoints upon executing a DDL operation. When #transaction
# is finished and tries to release the savepoint it created earlier, a
# database error will occur because the savepoint has already been
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
connection.increment_open_transactions
begin
connection.transaction(connection.open_transactions == 1, &block)
ensure
connection.decrement_open_transactions
end
def transaction(options = {}, &block)
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
end
......
......@@ -18,11 +18,43 @@ def test_nil_defaults_for_not_null_columns
end
end
if current_adapter?(:MysqlAdapter)
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
assert_equal 1, default.positive_integer
assert_instance_of Fixnum, default.negative_integer
assert_equal -1, default.negative_integer
assert_instance_of BigDecimal, default.decimal_number
assert_equal BigDecimal.new("2.78"), default.decimal_number
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
# older postgres versions represent the default with escapes ("\\012" for a newline)
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
end
end
end
#MySQL 5 and higher is quirky with not null text/blob columns.
#With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default
#but it behaves as though the column had a default of ''
if current_adapter?(:MysqlAdapter)
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
# open a new transaction. When in transactional fixtures mode, this will
# cause ActiveRecord to create a new savepoint. However, since MySQL doesn't
# support DDL transactions, creating a table will result in any created
# savepoints to be automatically released. This in turn causes the savepoint
# release code in AbstractAdapter#transaction to fail.
#
# We don't want that to happen, so we disable transactional fixtures here.
self.use_transactional_fixtures = false
# MySQL 5 and higher is quirky with not null text/blob columns.
# With MySQL Text/blob columns cannot have defaults. If the column is not
# null MySQL will report that the column has a null default
# but it behaves as though the column had a default of ''
def test_mysql_text_not_null_defaults
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'test_mysql_text_not_null_defaults'
......@@ -48,8 +80,7 @@ def test_mysql_text_not_null_defaults
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
# MySQL uses an implicit default 0 rather than NULL unless in strict mode.
# We use an implicit NULL so schema.rb is compatible with other databases.
def test_mysql_integer_not_null_defaults
......@@ -77,24 +108,4 @@ def test_mysql_integer_not_null_defaults
klass.connection.drop_table(klass.table_name) rescue nil
end
end
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
assert_equal 1, default.positive_integer
assert_instance_of Fixnum, default.negative_integer
assert_equal -1, default.negative_integer
assert_instance_of BigDecimal, default.decimal_number
assert_equal BigDecimal.new("2.78"), default.decimal_number
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
# older postgres versions represent the default with escapes ("\\012" for a newline)
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
end
end
end
......@@ -34,7 +34,7 @@ def uses_mocha(description)
end
ActiveRecord::Base.connection.class.class_eval do
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/]
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/]
def execute_with_query_record(sql, name = nil, &block)
$queries_executed ||= []
......
......@@ -213,11 +213,104 @@ def test_manually_rolling_back_a_transaction
assert Topic.find(2).approved?, "Second should still be approved"
end
def test_invalid_keys_for_transaction
assert_raises ArgumentError do
Topic.transaction :nested => true do
end
end
end
def test_force_savepoint_in_nested_transaction
Topic.transaction do
@first.approved = true
@second.approved = false
@first.save!
@second.save!
begin
Topic.transaction :requires_new => true do
@first.happy = false
@first.save!
raise
end
rescue
end
end
assert @first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
def test_no_savepoint_in_nested_transaction_without_force
Topic.transaction do
@first.approved = true
@second.approved = false
@first.save!
@second.save!
begin
Topic.transaction do
@first.approved = false
@first.save!
raise
end
rescue
end
end
assert !@first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
def test_many_savepoints
Topic.transaction do
@first.content = "One"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Two"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Three"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Four"
@first.save!
raise
end
rescue
end
@three = @first.reload.content
raise
end
rescue
end
@two = @first.reload.content
raise
end
rescue
end
@one = @first.reload.content
end
assert_equal "One", @one
assert_equal "Two", @two
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
uses_mocha 'mocking connection.commit_db_transaction' do
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
Topic.connection.expects(:outside_transaction?).returns(false)
Topic.connection.expects(:rollback_db_transaction)
assert_raise RuntimeError do
......@@ -227,6 +320,38 @@ def test_rollback_when_commit_raises
end
end
end
if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
def test_outside_transaction_works
assert Topic.connection.outside_transaction?
Topic.connection.begin_db_transaction
assert !Topic.connection.outside_transaction?
Topic.connection.rollback_db_transaction
assert Topic.connection.outside_transaction?
end
uses_mocha 'mocking connection.rollback_db_transaction' do
def test_rollback_wont_be_executed_if_no_transaction_active
assert_raise RuntimeError do
Topic.transaction do
Topic.connection.rollback_db_transaction
Topic.connection.expects(:rollback_db_transaction).never
raise "Rails doesn't scale!"
end
end
end
end
def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
Topic.transaction do
Topic.transaction do
Topic.connection.rollback_db_transaction
end
assert_equal 0, Topic.connection.open_transactions
end
assert_equal 0, Topic.connection.open_transactions
end
end
def test_sqlite_add_column_in_transaction_raises_statement_invalid
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
......@@ -282,6 +407,45 @@ def remove_exception_raising_after_create_callback_to_topic
end
end
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = true
fixtures :topics
def test_automatic_savepoint_in_outer_transaction
@first = Topic.find(1)
begin
Topic.transaction do
@first.approved = true
@first.save!
raise
end
rescue
assert !@first.reload.approved?
end
end
def test_no_automatic_savepoint_for_inner_transaction
@first = Topic.find(1)
Topic.transaction do
@first.approved = true
@first.save!
begin
Topic.transaction do
@first.approved = false
@first.save!
raise
end
rescue
end
end
assert !@first.reload.approved?
end
end if Topic.connection.supports_savepoints?
if current_adapter?(:PostgreSQLAdapter)
class ConcurrentTransactionTest < TransactionTest
use_concurrent_connections
......
*2.3.0 [Edge]*
* TimeWithZone#xmlschema accepts optional fraction_digits argument [#1725 state:resolved] [Nicholas Dainty]
* Object#tap shim for Ruby < 1.8.7. Similar to Object#returning, tap yields self then returns self. [Jeremy Kemper]
array.select { ... }.tap(&:inspect).map { ... }
......
......@@ -102,6 +102,6 @@ def acts_like?(duck)
# Person.try(:find, 1)
# @people.try(:map) {|p| p.name}
def try(method, *args, &block)
send(method, *args, &block) if respond_to?(method, true)
send(method, *args, &block) unless self.nil?
end
end
......@@ -99,8 +99,12 @@ def inspect
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
end
def xmlschema
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}"
def xmlschema(fraction_digits = 0)
fraction = if fraction_digits > 0
".%i" % time.usec.to_s[0, fraction_digits]
end
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
end
alias_method :iso8601, :xmlschema
......
......@@ -256,21 +256,13 @@ def setup
def test_nonexisting_method
method = :undefined_method
assert !@string.respond_to?(method)
assert_nil @string.try(method)
assert_raises(NoMethodError) { @string.try(method) }
end
def test_valid_method
assert_equal 5, @string.try(:size)
end
def test_valid_private_method
class << @string
private :size
end
assert_equal 5, @string.try(:size)
end
def test_argument_forwarding
assert_equal 'Hey', @string.try(:sub, 'llo', 'y')
end
......@@ -278,4 +270,13 @@ def test_argument_forwarding
def test_block_forwarding
assert_equal 'Hey', @string.try(:sub, 'llo') { |match| 'y' }
end
def test_nil_to_type
assert_nil nil.try(:to_s)
assert_nil nil.try(:to_i)
end
def test_false_try
assert_equal 'false', false.try(:to_s)
end
end
......@@ -105,6 +105,15 @@ def test_xmlschema
end
end
def test_xmlschema_with_fractional_seconds
silence_warnings do # silence warnings raised by tzinfo gem
@twz += 0.123456 # advance the time by a fraction of a second
assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3)
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6)
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12)
end
end
def test_to_yaml
silence_warnings do # silence warnings raised by tzinfo gem
assert_equal "--- 1999-12-31 19:00:00 -05:00\n", @twz.to_yaml
......
......@@ -5,7 +5,7 @@
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
ActionController::Base.session = {
:session_key => '_<%= app_name %>_session',
:key => '_<%= app_name %>_session',
:secret => '<%= app_secret %>'
}
......
......@@ -93,7 +93,7 @@ That means the security of this storage depends on this secret (and of the diges
....................................
config.action_controller.session = {
:session_key => ‘_app_session’,
:key => ‘_app_session’,
:secret => ‘0x0dkfj3927dkc7djdh36rkckdfzsg...’
}
....................................
......
......@@ -537,7 +537,7 @@ def initialize_i18n
end
def initialize_metal
configuration.middleware.insert_before(:"ActionController::VerbPiggybacking", Rails::Rack::Metal)
configuration.middleware.insert_before(:"ActionController::RewindableInput", Rails::Rack::Metal)
end
# Initializes framework-specific settings for each of the loaded frameworks
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册