diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2009-01-29 22:33:09 +0000 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2009-01-29 22:33:09 +0000 |
commit | 20d850dac78fc11512a6ad9f3d2ad397daac3e87 (patch) | |
tree | d982ec813560600d21edfe3ad1597d2c83af1df1 | |
parent | 6932ae4b2978de6771e6d1c84cfc3595cf9d8bab (diff) | |
parent | b3bc4fa5e02e71a992f8a432757548c762f0aad8 (diff) | |
download | rails-20d850dac78fc11512a6ad9f3d2ad397daac3e87.tar.gz rails-20d850dac78fc11512a6ad9f3d2ad397daac3e87.tar.bz2 rails-20d850dac78fc11512a6ad9f3d2ad397daac3e87.zip |
Merge commit 'mainstream/master'
75 files changed, 1262 insertions, 277 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 26e40e76d5..e9e18a8f6b 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,41 @@ *2.3.0 [Edge]* +* Added grouped_options_for_select helper method for wrapping option tags in optgroups. #977 [Jon Crawford] + +* Implement HTTP Digest authentication. #1230 [Gregg Kellogg, Pratik Naik] Example : + + class DummyDigestController < ActionController::Base + USERS = { "lifo" => 'world' } + + before_filter :authenticate + + def index + render :text => "Hello Secret" + end + + private + + def authenticate + authenticate_or_request_with_http_digest("Super Secret") do |username| + # Return the user's password + USERS[username] + end + end + end + +* Improved i18n support for the number_to_human_size helper. Changes the storage_units translation data; update your translations accordingly. #1634 [Yaroslav Markin] + storage_units: + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + * Added :silence option to BenchmarkHelper#benchmark and turned log_level into a hash parameter and deprecated the old use [DHH] * Fixed the AssetTagHelper cache to use the computed asset host as part of the cache key instead of just assuming the its a string #1299 [DHH] diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index dca07a0afc..3e77970367 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -63,7 +63,6 @@ module ActionController autoload :RecordIdentifier, 'action_controller/record_identifier' 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' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 7a380bd1fb..36b80d5780 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -644,7 +644,7 @@ module ActionController #:nodoc: end def session_enabled? - request.session_options && request.session_options[:disabled] != false + ActiveSupport::Deprecation.warn("Sessions are now lazy loaded. So if you don't access them, consider them disabled.", caller) end self.view_paths = [] @@ -1315,10 +1315,6 @@ module ActionController #:nodoc: "#{request.protocol}#{request.host}#{request.request_uri}" end - def close_session - @_session.close if @_session && @_session.respond_to?(:close) - end - def default_template(action_name = self.action_name) self.view_paths.find_template(default_template_name(action_name), default_template_format) end @@ -1342,15 +1338,14 @@ module ActionController #:nodoc: end def process_cleanup - close_session end end Base.class_eval do [ Filters, Layout, Benchmarking, Rescue, Flash, MimeResponds, Helpers, Cookies, Caching, Verification, Streaming, SessionManagement, - HttpAuthentication::Basic::ControllerMethods, RecordIdentifier, - RequestForgeryProtection, Translation + HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, + RecordIdentifier, RequestForgeryProtection, Translation ].each do |mod| include mod end diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb index c7992d7769..c1be264ffb 100644 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ b/actionpack/lib/action_controller/caching/sweeping.rb @@ -87,9 +87,9 @@ module ActionController #:nodoc: __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true) end - def method_missing(method, *arguments) + def method_missing(method, *arguments, &block) return if @controller.nil? - @controller.__send__(method, *arguments) + @controller.__send__(method, *arguments, &block) end end end diff --git a/actionpack/lib/action_controller/http_authentication.rb b/actionpack/lib/action_controller/http_authentication.rb index 2ed810db7d..5d915fda08 100644 --- a/actionpack/lib/action_controller/http_authentication.rb +++ b/actionpack/lib/action_controller/http_authentication.rb @@ -1,42 +1,42 @@ module ActionController module HttpAuthentication # Makes it dead easy to do HTTP Basic authentication. - # + # # Simple Basic example: - # + # # class PostsController < ApplicationController # USER_NAME, PASSWORD = "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_basic do |user_name, password| + # authenticate_or_request_with_http_basic do |user_name, password| # user_name == USER_NAME && password == PASSWORD # end # end # end - # - # - # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, + # + # + # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, # the regular HTML interface is protected by a session approach: - # + # # class ApplicationController < ActionController::Base # before_filter :set_account, :authenticate - # + # # protected # def set_account # @account = Account.find_by_url_name(request.subdomains.first) # end - # + # # def authenticate # case request.format # when Mime::XML, Mime::ATOM @@ -54,24 +54,48 @@ module ActionController # end # end # end - # - # + # # In your integration tests, you can do something like this: - # + # # def test_access_granted_from_xml # get( - # "/notes/1.xml", nil, + # "/notes/1.xml", nil, # :authorization => ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) # ) - # + # # assert_equal 200, status # end - # - # + # + # Simple Digest example: + # + # 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 |username| + # USERS[username] + # end + # end + # end + # + # NOTE: The +authenticate_or_request_with_http_digest+ 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. + # # On shared hosts, Apache sometimes doesn't pass authentication headers to # FCGI instances. If your environment matches this description and you cannot # authenticate, try this rule in your Apache setup: - # + # # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Basic extend self @@ -99,14 +123,14 @@ module ActionController def user_name_and_password(request) decode_credentials(request).split(/:/, 2) 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 - + def decode_credentials(request) ActiveSupport::Base64.decode64(authorization(request).split.last || '') end @@ -120,5 +144,131 @@ module ActionController controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized end end + + module Digest + extend self + + module ControllerMethods + def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) + authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm) + end + + # Authenticate with HTTP Digest, returns true or false + def authenticate_with_http_digest(realm = "Application", &password_procedure) + HttpAuthentication::Digest.authenticate(self, realm, &password_procedure) + end + + # Render output including the HTTP Digest authentication header + def request_http_digest_authentication(realm = "Application", message = nil) + HttpAuthentication::Digest.authentication_request(self, realm, message) + end + end + + # Returns false on a valid response, true otherwise + def authenticate(controller, realm, &password_procedure) + authorization(controller.request) && validate_digest_response(controller.request, realm, &password_procedure) + end + + def authorization(request) + request.env['HTTP_AUTHORIZATION'] || + request.env['X-HTTP_AUTHORIZATION'] || + request.env['X_HTTP_AUTHORIZATION'] || + request.env['REDIRECT_X_HTTP_AUTHORIZATION'] + end + + # Raises error unless the request credentials response value matches the expected value. + def validate_digest_response(request, realm, &password_procedure) + credentials = decode_credentials_header(request) + valid_nonce = validate_nonce(request, credentials[:nonce]) + + if valid_nonce && realm == credentials[:realm] && opaque(request.session.session_id) == credentials[:opaque] + password = password_procedure.call(credentials[:username]) + expected = expected_response(request.env['REQUEST_METHOD'], request.url, credentials, password) + expected == credentials[:response] + end + 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_header(request) + decode_credentials(authorization(request)) + end + + def decode_credentials(header) + header.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) + session_id = controller.request.session.session_id + controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(session_id)}", opaque="#{opaque(session_id)}") + end + + def authentication_request(controller, realm, message = nil) + message ||= "HTTP Digest: Access denied.\n" + authentication_header(controller, realm) + controller.__send__ :render, :text => message, :status => :unauthorized + end + + # Uses an MD5 digest based on time to generate a value to be used only once. + # + # A server-specified data string which should be uniquely generated each time a 401 response is made. + # It is recommended that this string be base64 or hexadecimal data. + # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed. + # + # The contents of the nonce are implementation dependent. + # The quality of the implementation depends on a good choice. + # A nonce might, for example, be constructed as the base 64 encoding of + # + # => time-stamp H(time-stamp ":" ETag ":" private-key) + # + # where time-stamp is a server-generated time or other non-repeating value, + # ETag is the value of the HTTP ETag header associated with the requested entity, + # and private-key is data known only to the server. + # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and + # reject the request if it did not match the nonce from that header or + # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity. + # The inclusion of the ETag prevents a replay request for an updated version of the resource. + # (Note: including the IP address of the client in the nonce would appear to offer the server the ability + # to limit the reuse of the nonce to the same client that originally got it. + # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm. + # Also, IP address spoofing is not that hard.) + # + # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to + # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for + # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 + # of this document. + # + # The nonce is opaque to the client. + def nonce(session_id, time = Time.now) + 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 + nonce(request.session.session_id, t) == value && (t - Time.now.to_i).abs <= 10 * 60 + end + + # Opaque based on digest of session_id + def opaque(session_id) + Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '') + end + end end end diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb index 159c5c7326..183d56c2e8 100644 --- a/actionpack/lib/action_controller/layout.rb +++ b/actionpack/lib/action_controller/layout.rb @@ -179,7 +179,7 @@ module ActionController #:nodoc: end def layout_list #:nodoc: - Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] } + Array(view_paths).sum([]) { |path| Dir["#{path.to_str}/layouts/**/*"] } end def find_layout(layout, *formats) #:nodoc: diff --git a/actionpack/lib/action_controller/middleware_stack.rb b/actionpack/lib/action_controller/middleware_stack.rb index b94bf6ec4a..dbc2fda41e 100644 --- a/actionpack/lib/action_controller/middleware_stack.rb +++ b/actionpack/lib/action_controller/middleware_stack.rb @@ -75,17 +75,22 @@ module ActionController block.call(self) if block_given? end - def insert(index, *objs) + def insert(index, *args, &block) index = self.index(index) unless index.is_a?(Integer) - objs = objs.map { |obj| Middleware.new(obj) } - super(index, *objs) + middleware = Middleware.new(*args, &block) + super(index, middleware) end alias_method :insert_before, :insert - def insert_after(index, *objs) + def insert_after(index, *args, &block) index = self.index(index) unless index.is_a?(Integer) - insert(index + 1, *objs) + insert(index + 1, *args, &block) + end + + def swap(target, *args, &block) + insert_before(target, *args, &block) + delete(target) end def use(*args, &block) diff --git a/actionpack/lib/action_controller/middlewares.rb b/actionpack/lib/action_controller/middlewares.rb index 30dbc26f11..f9cfc2b18e 100644 --- a/actionpack/lib/action_controller/middlewares.rb +++ b/actionpack/lib/action_controller/middlewares.rb @@ -4,8 +4,6 @@ use "Rack::Lock", :if => lambda { use "ActionController::Failsafe" -use "ActiveRecord::QueryCache", :if => lambda { defined?(ActiveRecord) } - ["ActionController::Session::CookieStore", "ActionController::Session::MemCacheStore", "ActiveRecord::SessionStore"].each do |store| @@ -18,6 +16,6 @@ use "ActiveRecord::QueryCache", :if => lambda { defined?(ActiveRecord) } ) end -use ActionController::RewindableInput -use ActionController::ParamsParser -use Rack::MethodOverride +use "ActionController::RewindableInput" +use "ActionController::ParamsParser" +use "Rack::MethodOverride" diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 09dcd684e8..935326c347 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -7,7 +7,6 @@ require 'action_controller/cgi_ext' module ActionController class Request < Rack::Request - extend ActiveSupport::Memoizable %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -33,9 +32,8 @@ module ActionController # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS # constant above, an UnknownHttpMethod exception is raised. def request_method - HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") + @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") end - memoize :request_method # Returns the HTTP request \method used for action processing as a # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this @@ -75,9 +73,8 @@ module ActionController # # request.headers["Content-Type"] # => "text/plain" def headers - ActionController::Http::Headers.new(@env) + @headers ||= ActionController::Http::Headers.new(@env) end - memoize :headers # Returns the content length of the request as an integer. def content_length @@ -89,32 +86,33 @@ module ActionController # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ - Mime::Type.lookup($1.strip.downcase) - else - nil + @content_type ||= begin + if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ + Mime::Type.lookup($1.strip.downcase) + else + nil + end end end - memoize :content_type # Returns the accepted MIME type for the request. def accepts - header = @env['HTTP_ACCEPT'].to_s.strip + @accepts ||= begin + header = @env['HTTP_ACCEPT'].to_s.strip - if header.empty? - [content_type, Mime::ALL].compact - else - Mime::Type.parse(header) + if header.empty? + [content_type, Mime::ALL].compact + else + Mime::Type.parse(header) + end end end - memoize :accepts def if_modified_since if since = env['HTTP_IF_MODIFIED_SINCE'] Time.rfc2822(since) rescue nil end end - memoize :if_modified_since def if_none_match env['HTTP_IF_NONE_MATCH'] @@ -248,25 +246,21 @@ EOM @env['REMOTE_ADDR'] end - memoize :remote_ip # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil end - memoize :server_software # Returns the complete URL used for this request. def url protocol + host_with_port + request_uri end - memoize :url # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol ssl? ? 'https://' : 'http://' end - memoize :protocol # Is this an SSL request? def ssl? @@ -286,14 +280,12 @@ EOM def host raw_host_with_port.sub(/:\d+$/, '') end - memoize :host # Returns a \host:\port string for this request, such as "example.com" or # "example.com:8080". def host_with_port "#{host}#{port_string}" end - memoize :host_with_port # Returns the port number of this request as an integer. def port @@ -303,7 +295,6 @@ EOM standard_port end end - memoize :port # Returns the standard \port number for this request's protocol. def standard_port @@ -341,7 +332,6 @@ EOM def query_string @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '') end - memoize :query_string # Returns the request URI, accounting for server idiosyncrasies. # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. @@ -367,7 +357,6 @@ EOM end end end - memoize :request_uri # Returns the interpreted \path to requested resource after all the installation # directory of this application was taken into account. @@ -376,7 +365,6 @@ EOM path.sub!(/\A#{ActionController::Base.relative_url_root}/, '') path end - memoize :path # Read the request \body. This is useful for web services that need to # work with raw requests directly. diff --git a/actionpack/lib/action_controller/rewindable_input.rb b/actionpack/lib/action_controller/rewindable_input.rb index 36f655c51e..cedfb7fd75 100644 --- a/actionpack/lib/action_controller/rewindable_input.rb +++ b/actionpack/lib/action_controller/rewindable_input.rb @@ -3,12 +3,12 @@ module ActionController class RewindableIO < ActiveSupport::BasicObject def initialize(io) @io = io - @rewindable = io.is_a?(StringIO) + @rewindable = io.is_a?(::StringIO) end def method_missing(method, *args, &block) unless @rewindable - @io = StringIO.new(@io.read) + @io = ::StringIO.new(@io.read) @rewindable = true end diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb index bf09fd33c5..9434c2e05e 100644 --- a/actionpack/lib/action_controller/session/abstract_store.rb +++ b/actionpack/lib/action_controller/session/abstract_store.rb @@ -102,8 +102,10 @@ module ActionController response = @app.call(env) session_data = env[ENV_SESSION_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) - options = env[ENV_SESSION_OPTIONS_KEY] + options = env[ENV_SESSION_OPTIONS_KEY] + + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after] + session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) if session_data.is_a?(AbstractStore::SessionHash) sid = session_data.id diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb index e061c4d4a1..5a728d1877 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -45,7 +45,7 @@ module ActionController :domain => nil, :path => "/", :expire_after => nil, - :httponly => false + :httponly => true }.freeze ENV_SESSION_KEY = "rack.session".freeze @@ -56,8 +56,6 @@ module ActionController class CookieOverflow < StandardError; end def initialize(app, options = {}) - options = options.dup - # Process legacy CGI options options = options.symbolize_keys if options.has_key?(:session_path) @@ -95,12 +93,14 @@ module ActionController status, headers, body = @app.call(env) session_data = env[ENV_SESSION_KEY] - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) + options = env[ENV_SESSION_OPTIONS_KEY] + + if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after] + session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) session_data = marshal(session_data.to_hash) raise CookieOverflow if session_data.size > MAX - options = env[ENV_SESSION_OPTIONS_KEY] cookie = Hash.new cookie[:value] = session_data unless options[:expire_after].nil? diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 22b97fc157..ea17363c47 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -35,7 +35,6 @@ module ActionController #:nodoc: def port=(number) @env["SERVER_PORT"] = number.to_i - port(true) end def action=(action_name) @@ -49,8 +48,6 @@ module ActionController #:nodoc: @env["REQUEST_URI"] = value @request_uri = nil @path = nil - request_uri(true) - path(true) end def request_uri=(uri) @@ -58,9 +55,13 @@ module ActionController #:nodoc: @path = uri.split("?").first end + def request_method=(method) + @request_method = method + end + def accept=(mime_types) @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") - accepts(true) + @accepts = nil end def if_modified_since=(last_modified) @@ -76,11 +77,11 @@ module ActionController #:nodoc: end def request_uri(*args) - @request_uri || super + @request_uri || super() end def path(*args) - @path || super + @path || super() end def assign_parameters(controller_path, action, parameters) @@ -107,7 +108,7 @@ module ActionController #:nodoc: def recycle! self.query_parameters = {} self.path_parameters = {} - unmemoize_all + @headers, @request_method, @accepts, @content_type = nil, nil, nil, nil end def user_agent=(user_agent) @@ -279,38 +280,47 @@ module ActionController #:nodoc: end end - class TestSession #:nodoc: + class TestSession < Hash #:nodoc: attr_accessor :session_id def initialize(attributes = nil) @session_id = '' - @attributes = attributes.nil? ? nil : attributes.stringify_keys - @saved_attributes = nil + attributes ||= {} + replace(attributes.stringify_keys) end def data - @attributes ||= @saved_attributes || {} + to_hash end def [](key) - data[key.to_s] + super(key.to_s) end def []=(key, value) - data[key.to_s] = value + super(key.to_s, value) end - def update - @saved_attributes = @attributes + def update(hash = nil) + if hash.nil? + ActiveSupport::Deprecation.warn('use replace instead', caller) + replace({}) + else + super(hash) + end end - def delete - @attributes = nil + def delete(key = nil) + if key.nil? + ActiveSupport::Deprecation.warn('use clear instead', caller) + clear + else + super(key.to_s) + end end def close - update - delete + ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller) end end diff --git a/actionpack/lib/action_controller/url_encoded_pair_parser.rb b/actionpack/lib/action_controller/url_encoded_pair_parser.rb index b63dca987d..57594c4259 100644 --- a/actionpack/lib/action_controller/url_encoded_pair_parser.rb +++ b/actionpack/lib/action_controller/url_encoded_pair_parser.rb @@ -46,7 +46,7 @@ module ActionController when Array value.map { |v| get_typed_value(v) } when Hash - if value.has_key?(:tempfile) + if value.has_key?(:tempfile) && value[:filename].any? upload = value[:tempfile] upload.extend(UploadedFile) upload.original_path = value[:filename] diff --git a/actionpack/lib/action_controller/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index d86e2db67d..bb6cb437b7 100644 --- a/actionpack/lib/action_controller/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb @@ -92,15 +92,12 @@ module ActionController # end # end module UrlWriter - # The default options for urls written by this writer. Typically a <tt>:host</tt> - # pair is provided. - mattr_accessor :default_url_options - self.default_url_options = {} - def self.included(base) #:nodoc: ActionController::Routing::Routes.install_helpers(base) base.mattr_accessor :default_url_options - base.default_url_options ||= default_url_options + + # The default options for urls written by this writer. Typically a <tt>:host</tt> pair is provided. + base.default_url_options ||= {} end # Generate a url based on the options provided, default_url_options and the diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 9ed50a9653..54c82cbd1d 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -277,6 +277,62 @@ module ActionView end end + # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but + # wraps them with <tt><optgroup></tt> tags. + # + # Parameters: + # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the + # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a + # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. + # Ex. ["North America",[["United States","US"],["Canada","CA"]]] + # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, + # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options + # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. + # * +prompt+ - set to true or a prompt string. When the select element doesn’t have a value yet, this + # prepends an option with a generic prompt — "Please select" — or the given prompt string. + # + # Sample usage (Array): + # grouped_options = [ + # ['North America', + # [['United States','US'],'Canada']], + # ['Europe', + # ['Denmark','Germany','France']] + # ] + # grouped_options_for_select(grouped_options) + # + # Sample usage (Hash): + # grouped_options = { + # 'North America' => [['United States','US], 'Canada'], + # 'Europe' => ['Denmark','Germany','France'] + # } + # grouped_options_for_select(grouped_options) + # + # Possible output: + # <optgroup label="Europe"> + # <option value="Denmark">Denmark</option> + # <option value="Germany">Germany</option> + # <option value="France">France</option> + # </optgroup> + # <optgroup label="North America"> + # <option value="US">United States</option> + # <option value="Canada">Canada</option> + # </optgroup> + # + # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to + # wrap the output in an appropriate <tt><select></tt> tag. + def grouped_options_for_select(grouped_options, selected_key = nil, prompt = nil) + body = '' + body << content_tag(:option, prompt, :value => "") if prompt + + grouped_options = grouped_options.sort if grouped_options.is_a?(Hash) + + grouped_options.each do |group| + body << content_tag(:optgroup, options_for_select(group[1], selected_key), :label => group[0]) + end + + body + end + # Returns a string of option tags for pretty much any time zone in the # world. Supply a TimeZone name as +selected+ to have it marked as the # selected option tag. You can also supply an array of TimeZone objects @@ -349,8 +405,9 @@ module ActionView html_options = html_options.stringify_keys add_default_name_and_id(html_options) value = value(object) + selected_value = options.has_key?(:selected) ? options[:selected] : value content_tag( - "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options + "select", add_options(options_from_collection_for_select(collection, value_method, text_method, selected_value), options, value), html_options ) end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 3e734ccaab..e622f97b9e 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -220,6 +220,8 @@ module ActionView end end + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze + # Formats the bytes in +size+ into a more understandable representation # (e.g., giving it 1500 yields 1.5 KB). This method is useful for # reporting file sizes to users. This method returns nil if @@ -247,7 +249,7 @@ module ActionView # number_to_human_size(1234567, 2) # => 1.18 MB # number_to_human_size(483989, 0) # => 473 KB def number_to_human_size(number, *args) - return number.nil? ? nil : pluralize(number.to_i, "Byte") if number.to_i < 1024 + return nil if number.nil? options = args.extract_options! options.symbolize_keys! @@ -255,7 +257,6 @@ module ActionView defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {} human = I18n.translate(:'number.human.format', :locale => options[:locale], :raise => true) rescue {} defaults = defaults.merge(human) - storage_units = I18n.translate(:'number.human.storage_units', :locale => options[:locale], :raise => true) unless args.empty? ActiveSupport::Deprecation.warn('number_to_human_size takes an option hash ' + @@ -267,22 +268,32 @@ module ActionView separator ||= (options[:separator] || defaults[:separator]) delimiter ||= (options[:delimiter] || defaults[:delimiter]) - max_exp = storage_units.size - 1 - number = Float(number) - exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024 - exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit - number /= 1024 ** exponent - unit = storage_units[exponent] + storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) - begin - escaped_separator = Regexp.escape(separator) - number_with_precision(number, - :precision => precision, - :separator => separator, - :delimiter => delimiter - ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + " #{unit}" - rescue - number + if number.to_i < 1024 + unit = I18n.translate(:'number.human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) + storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit) + else + max_exp = STORAGE_UNITS.size - 1 + number = Float(number) + exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024 + exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit + number /= 1024 ** exponent + + unit_key = STORAGE_UNITS[exponent] + unit = I18n.translate(:"number.human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) + + begin + escaped_separator = Regexp.escape(separator) + formatted_number = number_with_precision(number, + :precision => precision, + :separator => separator, + :delimiter => delimiter + ).sub(/(\d)(#{escaped_separator}[1-9]*)?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit) + rescue + number + end end end end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 1d9e4fe9b8..b1eb6891fa 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -107,7 +107,7 @@ module ActionView text else match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})/i, options[:highlighter]) + text.gsub(/(#{match})(?!(?:[^<]*?)?(?:["'])[^<>]*>)/i, options[:highlighter]) end end diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index a880fd83ef..afe35691bc 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -44,7 +44,18 @@ # separator: delimiter: "" precision: 1 - storage_units: [Bytes, KB, MB, GB, TB] + storage_units: + # Storage units output formatting. + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" # Used in distance_of_time_in_words(), distance_of_time_in_words_to_now(), time_ago_in_words() datetime: diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 19207e7262..ee26542a07 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -37,10 +37,17 @@ module ActionView #:nodoc: template_path = original_template_path.sub(/^\//, '') each do |load_path| - if format && (template = load_path["#{template_path}.#{format}"]) + if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"]) + return template + elsif format && (template = load_path["#{template_path}.#{format}"]) + return template + elsif template = load_path["#{template_path}.#{I18n.locale}"] return template elsif template = load_path[template_path] return template + # Try to find html version if the format is javascript + elsif format == :js && template = load_path["#{template_path}.html"] + return template end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 9d1e0d3ac5..1361a969a9 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -93,13 +93,14 @@ module ActionView #:nodoc: @@exempt_from_layout.merge(regexps) end - attr_accessor :filename, :load_path, :base_path, :name, :format, :extension + attr_accessor :filename, :load_path, :base_path + attr_accessor :locale, :name, :format, :extension delegate :to_s, :to => :path def initialize(template_path, load_paths = []) template_path = template_path.dup @load_path, @filename = find_full_path(template_path, load_paths) - @base_path, @name, @format, @extension = split(template_path) + @base_path, @name, @locale, @format, @extension = split(template_path) @base_path.to_s.gsub!(/\/$/, '') # Push to split method # Extend with partial super powers @@ -137,17 +138,17 @@ module ActionView #:nodoc: memoize :mime_type def path - [base_path, [name, format, extension].compact.join('.')].compact.join('/') + [base_path, [name, locale, format, extension].compact.join('.')].compact.join('/') end memoize :path def path_without_extension - [base_path, [name, format].compact.join('.')].compact.join('/') + [base_path, [name, locale, format].compact.join('.')].compact.join('/') end memoize :path_without_extension def path_without_format_and_extension - [base_path, name].compact.join('/') + [base_path, [name, locale].compact.join('.')].compact.join('/') end memoize :path_without_format_and_extension @@ -207,6 +208,10 @@ module ActionView #:nodoc: !Template.registered_template_handler(extension).nil? end + def valid_locale?(locale) + I18n.available_locales.include?(locale.to_sym) + end + def find_full_path(path, load_paths) load_paths = Array(load_paths) + [nil] load_paths.each do |load_path| @@ -217,19 +222,42 @@ module ActionView #:nodoc: end # Returns file split into an array - # [base_path, name, format, extension] + # [base_path, name, locale, format, extension] def split(file) - if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/) - if valid_extension?(m[5]) # Multipart formats - [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] - elsif valid_extension?(m[4]) # Single format - [m[1], m[2], m[3], m[4]] - elsif valid_extension?(m[3]) # No format - [m[1], m[2], nil, m[3]] + if m = file.match(/^(.*\/)?([^\.]+)\.(.*)$/) + base_path = m[1] + name = m[2] + extensions = m[3] + else + return + end + + locale = nil + format = nil + extension = nil + + if m = extensions.match(/^(\w+)?\.?(\w+)?\.?(\w+)?\.?/) + if valid_locale?(m[1]) && m[2] && valid_extension?(m[3]) # All three + locale = m[1] + format = m[2] + extension = m[3] + elsif m[1] && m[2] && valid_extension?(m[3]) # Multipart formats + format = "#{m[1]}.#{m[2]}" + extension = m[3] + elsif valid_locale?(m[1]) && valid_extension?(m[2]) # locale and extension + locale = m[1] + extension = m[2] + elsif valid_extension?(m[2]) # format and extension + format = m[1] + extension = m[2] + elsif valid_extension?(m[1]) # Just extension + extension = m[1] else # No extension - [m[1], m[2], m[3], nil] + format = m[1] end end + + [base_path, name, locale, format, extension] end end end diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 30e2d863d0..4baebcb4d1 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -32,6 +32,10 @@ ActionController::Routing::Routes.reload rescue nil ActionController::Base.session_store = nil +# Register danish language for testing +I18n.backend.store_translations 'da', {} +ORIGINAL_LOCALES = I18n.available_locales + FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures') ActionController::Base.view_paths = FIXTURE_LOAD_PATH diff --git a/actionpack/test/controller/http_authentication_test.rb b/actionpack/test/controller/http_authentication_test.rb deleted file mode 100644 index c0069e8032..0000000000 --- a/actionpack/test/controller/http_authentication_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'abstract_unit' - -class HttpBasicAuthenticationTest < Test::Unit::TestCase - include ActionController::HttpAuthentication::Basic - - class DummyController - attr_accessor :headers, :renders, :request - - def initialize - @headers, @renders = {}, [] - @request = ActionController::TestRequest.new - end - - def render(options) - self.renders << options - end - end - - def setup - @controller = DummyController.new - @credentials = ActionController::HttpAuthentication::Basic.encode_credentials("dhh", "secret") - end - - def test_successful_authentication - login = Proc.new { |user_name, password| user_name == "dhh" && password == "secret" } - set_headers - assert authenticate(@controller, &login) - - set_headers '' - assert_nothing_raised do - assert !authenticate(@controller, &login) - end - - set_headers nil - set_headers @credentials, 'REDIRECT_X_HTTP_AUTHORIZATION' - assert authenticate(@controller, &login) - end - - def test_failing_authentication - set_headers - assert !authenticate(@controller) { |user_name, password| user_name == "dhh" && password == "incorrect" } - end - - def test_authentication_request - authentication_request(@controller, "Megaglobalapp") - assert_equal 'Basic realm="Megaglobalapp"', @controller.headers["WWW-Authenticate"] - assert_equal :unauthorized, @controller.renders.first[:status] - end - - private - def set_headers(value = @credentials, name = 'HTTP_AUTHORIZATION') - @controller.request.env[name] = value - end -end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb new file mode 100644 index 0000000000..fbc94a0df7 --- /dev/null +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -0,0 +1,88 @@ +require 'abstract_unit' + +class HttpBasicAuthenticationTest < ActionController::TestCase + class DummyController < ActionController::Base + before_filter :authenticate, :only => :index + before_filter :authenticate_with_request, :only => :display + + def index + render :text => "Hello Secret" + end + + def display + render :text => 'Definitely Maybe' + end + + private + + def authenticate + authenticate_or_request_with_http_basic do |username, password| + username == 'lifo' && password == 'world' + end + end + + def authenticate_with_request + if authenticate_with_http_basic { |username, password| username == 'pretty' && password == 'please' } + @logged_in = true + else + request_http_basic_authentication("SuperSecret") + end + end + end + + AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'] + + tests DummyController + + AUTH_HEADERS.each do |header| + test "successful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials('lifo', 'world') + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}" + end + end + + AUTH_HEADERS.each do |header| + test "unsuccessful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials('h4x0r', 'world') + get :index + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" + end + end + + test "authentication request without credential" do + get :display + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] + end + + test "authentication request with invalid credential" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('pretty', 'foo') + get :display + + assert_response :unauthorized + assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] + end + + test "authentication request with valid credential" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('pretty', 'please') + get :display + + assert_response :success + assert assigns(:logged_in) + assert_equal 'Definitely Maybe', @response.body + end + + private + + def encode_credentials(username, password) + "Basic #{ActiveSupport::Base64.encode64("#{username}:#{password}")}" + end +end diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb new file mode 100644 index 0000000000..59f7a403b5 --- /dev/null +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -0,0 +1,130 @@ +require 'abstract_unit' + +class HttpDigestAuthenticationTest < ActionController::TestCase + class DummyDigestController < ActionController::Base + before_filter :authenticate, :only => :index + before_filter :authenticate_with_request, :only => :display + + USERS = { 'lifo' => 'world', 'pretty' => 'please' } + + def index + render :text => "Hello Secret" + end + + def display + render :text => 'Definitely Maybe' + end + + private + + def authenticate + authenticate_or_request_with_http_digest("SuperSecret") do |username| + # Return the password + USERS[username] + end + end + + def authenticate_with_request + if authenticate_with_http_digest("SuperSecret") { |username| USERS[username] } + @logged_in = true + else + request_http_digest_authentication("SuperSecret", "Authentication Failed") + end + end + end + + AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'] + + tests DummyDigestController + + AUTH_HEADERS.each do |header| + test "successful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials(:username => 'lifo', :password => 'world') + get :index + + assert_response :success + assert_equal 'Hello Secret', @response.body, "Authentication failed for request header #{header}" + end + end + + AUTH_HEADERS.each do |header| + test "unsuccessful authentication with #{header.downcase}" do + @request.env[header] = encode_credentials(:username => 'h4x0r', :password => 'world') + get :index + + assert_response :unauthorized + assert_equal "HTTP Digest: Access denied.\n", @response.body, "Authentication didn't fail for request header #{header}" + end + end + + test "authentication request without credential" do + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + credentials = decode_credentials(@response.headers['WWW-Authenticate']) + assert_equal 'SuperSecret', credentials[:realm] + end + + test "authentication request with invalid password" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'foo') + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with invalid nonce" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please', :nonce => "xxyyzz") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with invalid opaque" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'foo', :opaque => "xxyyzz") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with invalid realm" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'foo', :realm => "NotSecret") + get :display + + assert_response :unauthorized + assert_equal "Authentication Failed", @response.body + end + + test "authentication request with valid credential" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials(:username => 'pretty', :password => 'please') + get :display + + assert_response :success + assert assigns(:logged_in) + assert_equal 'Definitely Maybe', @response.body + end + + private + + def encode_credentials(options) + options.reverse_merge!(:nc => "00000001", :cnonce => "0a4f113b") + password = options.delete(:password) + + # Perform unautheticated get to retrieve digest parameters to use on subsequent request + get :index + + assert_response :unauthorized + + credentials = decode_credentials(@response.headers['WWW-Authenticate']) + credentials.merge!(options) + credentials.merge!(:uri => "http://#{@request.host}#{@request.env['REQUEST_URI']}") + ActionController::HttpAuthentication::Digest.encode_credentials("GET", credentials, password) + end + + def decode_credentials(header) + ActionController::HttpAuthentication::Digest.decode_credentials(@response.headers['WWW-Authenticate']) + end +end
\ No newline at end of file diff --git a/actionpack/test/controller/middleware_stack_test.rb b/actionpack/test/controller/middleware_stack_test.rb index 5029f5f609..2a141697da 100644 --- a/actionpack/test/controller/middleware_stack_test.rb +++ b/actionpack/test/controller/middleware_stack_test.rb @@ -60,6 +60,12 @@ class MiddlewareStackTest < ActiveSupport::TestCase assert_equal BazMiddleware, @stack[2].klass end + test "swaps one middleware out for another" do + assert_equal FooMiddleware, @stack[0].klass + @stack.swap(FooMiddleware, BazMiddleware) + assert_equal BazMiddleware, @stack[0].klass + end + test "active returns all only enabled middleware" do assert_no_difference "@stack.active.size" do assert_difference "@stack.size" do diff --git a/actionpack/test/controller/rack_test.rb b/actionpack/test/controller/rack_test.rb index 8ad42614b4..e458ab6738 100644 --- a/actionpack/test/controller/rack_test.rb +++ b/actionpack/test/controller/rack_test.rb @@ -63,61 +63,61 @@ end class RackRequestTest < BaseRackTest def test_proxy_request - assert_equal 'glu.ttono.us', @request.host_with_port(true) + assert_equal 'glu.ttono.us', @request.host_with_port end def test_http_host @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "rubyonrails.org:8080" - assert_equal "rubyonrails.org", @request.host(true) - assert_equal "rubyonrails.org:8080", @request.host_with_port(true) + assert_equal "rubyonrails.org", @request.host + assert_equal "rubyonrails.org:8080", @request.host_with_port @env['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" - assert_equal "www.secondhost.org", @request.host(true) + assert_equal "www.secondhost.org", @request.host end def test_http_host_with_default_port_overrides_server_port @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "rubyonrails.org" - assert_equal "rubyonrails.org", @request.host_with_port(true) + assert_equal "rubyonrails.org", @request.host_with_port end def test_host_with_port_defaults_to_server_name_if_no_host_headers @env.delete "HTTP_X_FORWARDED_HOST" @env.delete "HTTP_HOST" - assert_equal "glu.ttono.us:8007", @request.host_with_port(true) + assert_equal "glu.ttono.us:8007", @request.host_with_port end def test_host_with_port_falls_back_to_server_addr_if_necessary @env.delete "HTTP_X_FORWARDED_HOST" @env.delete "HTTP_HOST" @env.delete "SERVER_NAME" - assert_equal "207.7.108.53", @request.host(true) - assert_equal 8007, @request.port(true) - assert_equal "207.7.108.53:8007", @request.host_with_port(true) + assert_equal "207.7.108.53", @request.host + assert_equal 8007, @request.port + assert_equal "207.7.108.53:8007", @request.host_with_port end def test_host_with_port_if_http_standard_port_is_specified @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:80" - assert_equal "glu.ttono.us", @request.host_with_port(true) + assert_equal "glu.ttono.us", @request.host_with_port end def test_host_with_port_if_https_standard_port_is_specified @env['HTTP_X_FORWARDED_PROTO'] = "https" @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:443" - assert_equal "glu.ttono.us", @request.host_with_port(true) + assert_equal "glu.ttono.us", @request.host_with_port end def test_host_if_ipv6_reference @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]" - assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host(true) + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host end def test_host_if_ipv6_reference_with_port @env.delete "HTTP_X_FORWARDED_HOST" @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]:8008" - assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host(true) + assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host end def test_cgi_environment_variables diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 8809aa7c34..584b9277c4 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -274,6 +274,9 @@ class TestController < ActionController::Base def render_explicit_html_template end + def render_implicit_html_template_from_xhr_request + end + def formatted_html_erb end @@ -1010,6 +1013,11 @@ class RenderTest < ActionController::TestCase end end + def test_should_implicitly_render_html_template_from_xhr_request + get :render_implicit_html_template_from_xhr_request, :format => :js + assert_equal "Hello HTML!", @response.body + end + def test_should_render_formatted_template get :formatted_html_erb assert_equal 'formatted html erb', @response.body diff --git a/actionpack/test/controller/request/multipart_params_parsing_test.rb b/actionpack/test/controller/request/multipart_params_parsing_test.rb index d7ade40f71..054519d0d2 100644 --- a/actionpack/test/controller/request/multipart_params_parsing_test.rb +++ b/actionpack/test/controller/request/multipart_params_parsing_test.rb @@ -101,6 +101,21 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest assert_equal 19756, files.size end + test "does not create tempfile if no file has been selected" do + params = parse_multipart('none') + assert_equal %w(files submit-name), params.keys.sort + assert_equal 'Larry', params['submit-name'] + assert_equal nil, params['files'] + end + + test "parses empty upload file" do + params = parse_multipart('empty') + assert_equal %w(files submit-name), params.keys.sort + assert_equal 'Larry', params['submit-name'] + assert params['files'] + assert_equal "", params['files'].read + end + test "uploads and reads binary file" do with_test_routing do fixture = FIXTURE_PATH + "/mona_lisa.jpg" diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb index 7097d08076..efe4f136f5 100644 --- a/actionpack/test/controller/request_test.rb +++ b/actionpack/test/controller/request_test.rb @@ -14,53 +14,53 @@ class RequestTest < ActiveSupport::TestCase assert_equal '0.0.0.0', @request.remote_ip @request.remote_addr = '1.2.3.4' - assert_equal '1.2.3.4', @request.remote_ip(true) + assert_equal '1.2.3.4', @request.remote_ip @request.remote_addr = '1.2.3.4,3.4.5.6' - assert_equal '1.2.3.4', @request.remote_ip(true) + assert_equal '1.2.3.4', @request.remote_ip @request.env['HTTP_CLIENT_IP'] = '2.3.4.5' - assert_equal '1.2.3.4', @request.remote_ip(true) + assert_equal '1.2.3.4', @request.remote_ip @request.remote_addr = '192.168.0.1' - assert_equal '2.3.4.5', @request.remote_ip(true) + assert_equal '2.3.4.5', @request.remote_ip @request.env.delete 'HTTP_CLIENT_IP' @request.remote_addr = '1.2.3.4' @request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6' - assert_equal '1.2.3.4', @request.remote_ip(true) + assert_equal '1.2.3.4', @request.remote_ip @request.remote_addr = '127.0.0.1' @request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '172.16.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '192.168.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1, 10.0.0.1, 3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,3.4.5.6' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,192.168.0.1' - assert_equal 'unknown', @request.remote_ip(true) + assert_equal 'unknown', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4' - assert_equal '3.4.5.6', @request.remote_ip(true) + assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_CLIENT_IP'] = '8.8.8.8' e = assert_raises(ActionController::ActionControllerError) { - @request.remote_ip(true) + @request.remote_ip } assert_match /IP spoofing attack/, e.message assert_match /HTTP_X_FORWARDED_FOR="9.9.9.9, 3.4.5.6, 10.0.0.1, 172.31.4.4"/, e.message @@ -72,11 +72,11 @@ class RequestTest < ActiveSupport::TestCase # leap of faith to assume that their proxies are ever going to set the # HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR headers properly. ActionController::Base.ip_spoofing_check = false - assert_equal('8.8.8.8', @request.remote_ip(true)) + assert_equal('8.8.8.8', @request.remote_ip) ActionController::Base.ip_spoofing_check = true @request.env['HTTP_X_FORWARDED_FOR'] = '8.8.8.8, 9.9.9.9' - assert_equal '8.8.8.8', @request.remote_ip(true) + assert_equal '8.8.8.8', @request.remote_ip @request.env.delete 'HTTP_CLIENT_IP' @request.env.delete 'HTTP_X_FORWARDED_FOR' @@ -189,8 +189,8 @@ class RequestTest < ActiveSupport::TestCase @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = "/path/dispatch.rb" @request.set_REQUEST_URI nil - assert_equal "/path/of/some/uri?mapped=1", @request.request_uri(true) - assert_equal "/of/some/uri", @request.path(true) + assert_equal "/path/of/some/uri?mapped=1", @request.request_uri + assert_equal "/of/some/uri", @request.path ActionController::Base.relative_url_root = nil @request.env['PATH_INFO'] = "/path/of/some/uri" @@ -225,12 +225,12 @@ class RequestTest < ActiveSupport::TestCase @request.set_REQUEST_URI '/hieraki/dispatch.cgi' ActionController::Base.relative_url_root = '/hieraki' - assert_equal "/dispatch.cgi", @request.path(true) + assert_equal "/dispatch.cgi", @request.path ActionController::Base.relative_url_root = nil @request.set_REQUEST_URI '/hieraki/dispatch.cgi' ActionController::Base.relative_url_root = '/foo' - assert_equal "/hieraki/dispatch.cgi", @request.path(true) + assert_equal "/hieraki/dispatch.cgi", @request.path ActionController::Base.relative_url_root = nil # This test ensures that Rails uses REQUEST_URI over PATH_INFO @@ -238,8 +238,8 @@ class RequestTest < ActiveSupport::TestCase @request.env['REQUEST_URI'] = "/some/path" @request.env['PATH_INFO'] = "/another/path" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" - assert_equal "/some/path", @request.request_uri(true) - assert_equal "/some/path", @request.path(true) + assert_equal "/some/path", @request.request_uri + assert_equal "/some/path", @request.path end def test_host_with_default_port @@ -255,13 +255,13 @@ class RequestTest < ActiveSupport::TestCase end def test_server_software - assert_equal nil, @request.server_software(true) + assert_equal nil, @request.server_software @request.env['SERVER_SOFTWARE'] = 'Apache3.422' - assert_equal 'apache', @request.server_software(true) + assert_equal 'apache', @request.server_software @request.env['SERVER_SOFTWARE'] = 'lighttpd(1.1.4)' - assert_equal 'lighttpd', @request.server_software(true) + assert_equal 'lighttpd', @request.server_software end def test_xml_http_request @@ -299,13 +299,13 @@ class RequestTest < ActiveSupport::TestCase def test_invalid_http_method_raises_exception assert_raises(ActionController::UnknownHttpMethod) do self.request_method = :random_method + @request.request_method end end def test_allow_method_hacking_on_post [:get, :head, :options, :put, :post, :delete].each do |method| self.request_method = method - @request.request_method(true) assert_equal(method == :head ? :get : method, @request.method) end end @@ -313,7 +313,7 @@ class RequestTest < ActiveSupport::TestCase def test_invalid_method_hacking_on_post_raises_exception assert_raises(ActionController::UnknownHttpMethod) do self.request_method = :_random_method - @request.request_method(true) + @request.request_method end end @@ -402,6 +402,6 @@ class RequestTest < ActiveSupport::TestCase protected def request_method=(method) @request.env['REQUEST_METHOD'] = method.to_s.upcase - @request.request_method(true) + @request.request_method = nil # Reset the ivar cache end end diff --git a/actionpack/test/controller/session/cookie_store_test.rb b/actionpack/test/controller/session/cookie_store_test.rb index d349c18d1d..95d2eb11c4 100644 --- a/actionpack/test/controller/session/cookie_store_test.rb +++ b/actionpack/test/controller/session/cookie_store_test.rb @@ -6,13 +6,11 @@ class CookieStoreTest < ActionController::IntegrationTest SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' DispatcherApp = ActionController::Dispatcher.new - CookieStoreApp = ActionController::Session::CookieStore.new(DispatcherApp, - :key => SessionKey, :secret => SessionSecret) + CookieStoreApp = ActionController::Session::CookieStore.new(DispatcherApp, :key => SessionKey, :secret => SessionSecret) Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, 'SHA1') - SignedBar = "BAh7BjoIZm9vIghiYXI%3D--" + - "fef868465920f415f2c0652d6910d3af288a0367" + SignedBar = "BAh7BjoIZm9vIghiYXI%3D--fef868465920f415f2c0652d6910d3af288a0367" class TestController < ActionController::Base def no_session_access @@ -94,7 +92,7 @@ class CookieStoreTest < ActionController::IntegrationTest with_test_route_set do get '/set_session_value' assert_response :success - assert_equal ["_myapp_session=#{response.body}; path=/"], + assert_equal ["_myapp_session=#{response.body}; path=/; httponly"], headers['Set-Cookie'] end end @@ -148,7 +146,7 @@ class CookieStoreTest < ActionController::IntegrationTest get '/set_session_value' assert_response :success session_payload = response.body - assert_equal ["_myapp_session=#{response.body}; path=/"], + assert_equal ["_myapp_session=#{response.body}; path=/; httponly"], headers['Set-Cookie'] get '/call_reset_session' @@ -177,6 +175,36 @@ class CookieStoreTest < ActionController::IntegrationTest end end + def test_session_store_with_expire_after + app = ActionController::Session::CookieStore.new(DispatcherApp, :key => SessionKey, :secret => SessionSecret, :expire_after => 5.hours) + @integration_session = open_session(app) + + with_test_route_set do + # First request accesses the session + time = Time.local(2008, 4, 24) + Time.stubs(:now).returns(time) + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d-%b-%Y %H:%M:%S GMT") + + cookies[SessionKey] = SignedBar + + get '/set_session_value' + assert_response :success + + cookie_body = response.body + assert_equal ["_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; httponly"], headers['Set-Cookie'] + + # Second request does not access the session + time = Time.local(2008, 4, 25) + Time.stubs(:now).returns(time) + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d-%b-%Y %H:%M:%S GMT") + + get '/no_session_access' + assert_response :success + + assert_equal ["_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; httponly"], headers['Set-Cookie'] + end + end + private def with_test_route_set with_routing do |set| diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb index e9d372544e..09a8356fec 100644 --- a/actionpack/test/controller/url_rewriter_test.rb +++ b/actionpack/test/controller/url_rewriter_test.rb @@ -303,7 +303,6 @@ class UrlWriterTests < ActionController::TestCase def test_named_routes_with_nil_keys ActionController::Routing::Routes.clear! - add_host! ActionController::Routing::Routes.draw do |map| map.main '', :controller => 'posts' map.resources :posts @@ -311,6 +310,8 @@ class UrlWriterTests < ActionController::TestCase end # We need to create a new class in order to install the new named route. kls = Class.new { include ActionController::UrlWriter } + kls.default_url_options[:host] = 'www.basecamphq.com' + controller = kls.new params = {:action => :index, :controller => :posts, :format => :xml} assert_equal("http://www.basecamphq.com/posts.xml", controller.send(:url_for, params)) @@ -337,6 +338,20 @@ class UrlWriterTests < ActionController::TestCase ensure ActionController::Routing::Routes.load! end + + def test_multiple_includes_maintain_distinct_options + first_class = Class.new { include ActionController::UrlWriter } + second_class = Class.new { include ActionController::UrlWriter } + + first_host, second_host = 'firsthost.com', 'secondhost.com' + + first_class.default_url_options[:host] = first_host + second_class.default_url_options[:host] = second_host + + assert_equal first_class.default_url_options[:host], first_host + assert_equal second_class.default_url_options[:host], second_host + end + private def extract_params(url) url.split('?', 2).last.split('&') diff --git a/actionpack/test/fixtures/multipart/empty b/actionpack/test/fixtures/multipart/empty new file mode 100644 index 0000000000..f0f79835c9 --- /dev/null +++ b/actionpack/test/fixtures/multipart/empty @@ -0,0 +1,10 @@ +--AaB03x
+Content-Disposition: form-data; name="submit-name"
+
+Larry
+--AaB03x
+Content-Disposition: form-data; name="files"; filename="file1.txt"
+Content-Type: text/plain
+
+
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/none b/actionpack/test/fixtures/multipart/none new file mode 100644 index 0000000000..d66f4730f1 --- /dev/null +++ b/actionpack/test/fixtures/multipart/none @@ -0,0 +1,9 @@ +--AaB03x
+Content-Disposition: form-data; name="submit-name"
+
+Larry
+--AaB03x
+Content-Disposition: form-data; name="files"; filename=""
+
+
+--AaB03x--
diff --git a/actionpack/test/fixtures/replies.yml b/actionpack/test/fixtures/replies.yml index a17d2fc42b..66020b706a 100644 --- a/actionpack/test/fixtures/replies.yml +++ b/actionpack/test/fixtures/replies.yml @@ -12,4 +12,4 @@ another: developer_id: 1 content: Nuh uh! created_at: <%= 1.hour.ago.to_s(:db) %> - updated_at: nil
\ No newline at end of file + updated_at: nil diff --git a/actionpack/test/fixtures/test/hello_world.da.html.erb b/actionpack/test/fixtures/test/hello_world.da.html.erb new file mode 100644 index 0000000000..10ec443291 --- /dev/null +++ b/actionpack/test/fixtures/test/hello_world.da.html.erb @@ -0,0 +1 @@ +Hey verden
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb new file mode 100644 index 0000000000..4a11845cfe --- /dev/null +++ b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb @@ -0,0 +1 @@ +Hello HTML!
\ No newline at end of file diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 86a0bb6a79..86b321e6e5 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -143,6 +143,32 @@ uses_mocha "FormOptionsHelperTest" do ) end + def test_grouped_options_for_select_with_array + assert_dom_equal( + "<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>", + grouped_options_for_select([ + ["North America", + [['United States','US'],"Canada"]], + ["Europe", + [["Great Britain","GB"], "Germany"]] + ]) + ) + end + + def test_grouped_options_for_select_with_selected_and_prompt + assert_dom_equal( + "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>", + grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", "Choose a product...") + ) + end + + def test_optgroups_with_with_options_with_hash + assert_dom_equal( + "<optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup><optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup>", + grouped_options_for_select({'North America' => ['United States','Canada'], 'Europe' => ['Denmark','Germany']}) + ) + end + def test_time_zone_options_no_parms opts = time_zone_options_for_select assert_dom_equal "<option value=\"A\">A</option>\n" + @@ -473,6 +499,22 @@ uses_mocha "FormOptionsHelperTest" do assert_dom_equal expected, collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true) end + def test_collection_select_with_blank_and_selected + @posts = [ + Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"), + Post.new("Babe went home", "Babe", "To a little house", "shh!"), + Post.new("Cabe went home", "Cabe", "To a little house", "shh!") + ] + + @post = Post.new + @post.author_name = "Babe" + + assert_dom_equal( + %{<select id="post_author_name" name="post[author_name]"><option value=""></option>\n<option value="<Abe>" selected="selected"><Abe></option>\n<option value="Babe">Babe</option>\n<option value="Cabe">Cabe</option></select>}, + collection_select("post", "author_name", @posts, "author_name", "author_name", {:include_blank => true, :selected => "<Abe>"}) + ) + end + def test_time_zone_select @firm = Firm.new("D") html = time_zone_select( "firm", "time_zone" ) diff --git a/actionpack/test/template/number_helper_i18n_test.rb b/actionpack/test/template/number_helper_i18n_test.rb index 2528bead36..3fdf991a44 100644 --- a/actionpack/test/template/number_helper_i18n_test.rb +++ b/actionpack/test/template/number_helper_i18n_test.rb @@ -10,7 +10,9 @@ class NumberHelperI18nTests < Test::Unit::TestCase @number_defaults = { :precision => 3, :delimiter => ',', :separator => '.' } @currency_defaults = { :unit => '$', :format => '%u%n', :precision => 2 } @human_defaults = { :precision => 1 } - @human_storage_units_defaults = %w(Bytes KB MB GB TB) + @human_storage_units_format_default = "%n %u" + @human_storage_units_units_byte_other = "Bytes" + @human_storage_units_units_kb_other = "KB" @percentage_defaults = { :delimiter => '' } @precision_defaults = { :delimiter => '' } @@ -48,10 +50,22 @@ class NumberHelperI18nTests < Test::Unit::TestCase I18n.expects(:translate).with(:'number.format', :locale => 'en', :raise => true).returns(@number_defaults) I18n.expects(:translate).with(:'number.human.format', :locale => 'en', :raise => true).returns(@human_defaults) - I18n.expects(:translate).with(:'number.human.storage_units', :locale => 'en', - :raise => true).returns(@human_storage_units_defaults) - # can't be called with 1 because this directly returns without calling I18n.translate - number_to_human_size(1025, :locale => 'en') + I18n.expects(:translate).with(:'number.human.storage_units.format', :locale => 'en', + :raise => true).returns(@human_storage_units_format_default) + I18n.expects(:translate).with(:'number.human.storage_units.units.kb', :locale => 'en', :count => 2, + :raise => true).returns(@human_storage_units_units_kb_other) + # 2KB + number_to_human_size(2048, :locale => 'en') + + I18n.expects(:translate).with(:'number.format', :locale => 'en', :raise => true).returns(@number_defaults) + I18n.expects(:translate).with(:'number.human.format', :locale => 'en', + :raise => true).returns(@human_defaults) + I18n.expects(:translate).with(:'number.human.storage_units.format', :locale => 'en', + :raise => true).returns(@human_storage_units_format_default) + I18n.expects(:translate).with(:'number.human.storage_units.units.byte', :locale => 'en', :count => 42, + :raise => true).returns(@human_storage_units_units_byte_other) + # 42 Bytes + number_to_human_size(42, :locale => 'en') end end end diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 4bd897efeb..c226e212b5 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -5,6 +5,13 @@ module RenderTestCases def setup_view(paths) @assigns = { :secret => 'in the sauce' } @view = ActionView::Base.new(paths, @assigns) + + # Reload and register danish language for testing + I18n.reload! + I18n.backend.store_translations 'da', {} + + # Ensure original are still the same since we are reindexing view paths + assert_equal ORIGINAL_LOCALES, I18n.available_locales end def test_render_file @@ -19,6 +26,14 @@ module RenderTestCases assert_equal "Hello world!", @view.render(:file => "test/hello_world") end + def test_render_file_with_localization + old_locale = I18n.locale + I18n.locale = :da + assert_equal "Hey verden", @view.render(:file => "test/hello_world") + ensure + I18n.locale = old_locale + end + def test_render_file_at_top_level assert_equal 'Elastica', @view.render(:file => '/shared') end diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index a6200fbdd7..564845779f 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -122,6 +122,29 @@ class TextHelperTest < ActionView::TestCase ) end + def test_highlight_with_html + assert_equal( + "<p>This is a <strong class=\"highlight\">beautiful</strong> morning, but also a <strong class=\"highlight\">beautiful</strong> day</p>", + highlight("<p>This is a beautiful morning, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<p>This is a <em><strong class=\"highlight\">beautiful</strong></em> morning, but also a <strong class=\"highlight\">beautiful</strong> day</p>", + highlight("<p>This is a <em>beautiful</em> morning, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<p>This is a <em class=\"error\"><strong class=\"highlight\">beautiful</strong></em> morning, but also a <strong class=\"highlight\">beautiful</strong> <span class=\"last\">day</span></p>", + highlight("<p>This is a <em class=\"error\">beautiful</em> morning, but also a beautiful <span class=\"last\">day</span></p>", "beautiful") + ) + assert_equal( + "<p class=\"beautiful\">This is a <strong class=\"highlight\">beautiful</strong> morning, but also a <strong class=\"highlight\">beautiful</strong> day</p>", + highlight("<p class=\"beautiful\">This is a beautiful morning, but also a beautiful day</p>", "beautiful") + ) + assert_equal( + "<p>This is a <strong class=\"highlight\">beautiful</strong> <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a <strong class=\"highlight\">beautiful</strong> day</p>", + highlight("<p>This is a beautiful <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a beautiful day</p>", "beautiful") + ) + end + def test_excerpt assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", 5)) assert_equal("This is a...", excerpt("This is a beautiful morning", "this", 5)) diff --git a/activerecord/Rakefile b/activerecord/Rakefile index f47674d5b7..1c7e2603ee 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -32,9 +32,13 @@ task :default => :test desc 'Run mysql, sqlite, and postgresql tests' task :test => %w(test_mysql test_sqlite3 test_postgresql) -for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase ) +for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ) Rake::TestTask.new("test_#{adapter}") { |t| - t.libs << "test" << "test/connections/native_#{adapter}" + if adapter =~ /jdbc/ + t.libs << "test" << "test/connections/jdbc_#{adapter}" + else + t.libs << "test" << "test/connections/native_#{adapter}" + end adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z]+/] t.test_files=Dir.glob( "test/cases/**/*_test{,_#{adapter_short}}.rb" ).sort t.verbose = true diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index ebc0b7783f..0efccb66ee 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -927,7 +927,7 @@ module ActiveRecord #:nodoc: # # ==== Parameters # - # * +id+ - The id of the object you wish to update a counter on. + # * +id+ - The id of the object you wish to update a counter on or an Array of ids. # * +counters+ - An Array of Hashes containing the names of the fields # to update as keys and the amount to update the field by as values. # @@ -941,12 +941,27 @@ module ActiveRecord #:nodoc: # # SET comment_count = comment_count - 1, # # action_count = action_count + 1 # # WHERE id = 5 + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # Post.update_counters [10, 15], :comment_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = comment_count + 1, + # # WHERE id IN (10, 15) def update_counters(id, counters) updates = counters.inject([]) { |list, (counter_name, increment)| sign = increment < 0 ? "-" : "+" list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}" }.join(", ") - update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}") + + if id.is_a?(Array) + ids_list = id.map {|i| quote_value(i)}.join(', ') + condition = "IN (#{ids_list})" + else + condition = "= #{quote_value(id)}" + end + + update_all(updates, "#{connection.quote_column_name(primary_key)} #{condition}") end # Increment a number field by one, usually representing a count. @@ -1700,7 +1715,7 @@ module ActiveRecord #:nodoc: end join end - joins.flatten.uniq + joins.flatten.map{|j| j.strip}.uniq else joins.collect{|j| safe_to_array(j)}.flatten.uniq end @@ -2097,7 +2112,11 @@ module ActiveRecord #:nodoc: (hash[method].keys + params.keys).uniq.each do |key| merge = hash[method][key] && params[key] # merge if both scopes have the same key if key == :conditions && merge - hash[method][key] = merge_conditions(params[key], hash[method][key]) + if params[key].is_a?(Hash) && hash[method][key].is_a?(Hash) + hash[method][key] = merge_conditions(hash[method][key].deep_merge(params[key])) + else + hash[method][key] = merge_conditions(params[key], hash[method][key]) + end elsif key == :include && merge hash[method][key] = merge_includes(hash[method][key], params[key]).uniq elsif key == :joins && merge @@ -2107,7 +2126,7 @@ module ActiveRecord #:nodoc: end end else - hash[method] = params.merge(hash[method]) + hash[method] = hash[method].merge(params) end else hash[method] = params diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index b2345fd571..9300df28ee 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -152,6 +152,7 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -563,8 +564,6 @@ module ActiveRecord private def connect - @connection.reconnect = true if @connection.respond_to?(:reconnect=) - encoding = @config[:encoding] if encoding @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil @@ -575,6 +574,10 @@ module ActiveRecord end @connection.real_connect(*@connection_options) + + # reconnect must be set after real_connect is called, because real_connect sets it to false internally + @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) + configure_connection end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 9387cf8827..5390f49f04 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -306,7 +306,7 @@ module ActiveRecord end def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => !columns(from).detect{|c| c.name == 'id'}.nil?) + options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) create_table(to, options) do |definition| @definition = definition columns(from).each do |column| diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 6a9690ba85..6d750accb0 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -904,7 +904,7 @@ module ActiveRecord configuration.update(attr_names.extract_options!) validates_each(attr_names, configuration) do |record, attr_name, value| - unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all? record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 0f03dae829..973bb567bd 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -639,6 +639,13 @@ class BasicsTest < ActiveRecord::TestCase category.reload assert_not_nil category.categorizations_count assert_equal 4, category.categorizations_count + + category_2 = categories(:technology) + count_1, count_2 = (category.categorizations_count || 0), (category_2.categorizations_count || 0) + Category.update_counters([category.id, category_2.id], "categorizations_count" => 2) + category.reload; category_2.reload + assert_equal count_1 + 2, category.categorizations_count + assert_equal count_2 + 2, category_2.categorizations_count end def test_update_all diff --git a/activerecord/test/cases/connection_test_mysql.rb b/activerecord/test/cases/connection_test_mysql.rb index 40ddcf5420..f79ee2f1f7 100644 --- a/activerecord/test/cases/connection_test_mysql.rb +++ b/activerecord/test/cases/connection_test_mysql.rb @@ -2,9 +2,24 @@ require "cases/helper" class MysqlConnectionTest < ActiveRecord::TestCase def setup + super @connection = ActiveRecord::Base.connection end + def test_mysql_reconnect_attribute_after_connection_with_reconnect_true + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true})) + assert ActiveRecord::Base.connection.raw_connection.reconnect + end + end + + def test_mysql_reconnect_attribute_after_connection_with_reconnect_false + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false})) + assert !ActiveRecord::Base.connection.raw_connection.reconnect + end + end + def test_no_automatic_reconnection_after_timeout assert @connection.active? @connection.update('set @@wait_timeout=1') @@ -27,4 +42,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase @connection.verify! assert @connection.active? end + + private + + def run_without_connection + original_connection = ActiveRecord::Base.remove_connection + begin + yield original_connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end + end end diff --git a/activerecord/test/cases/copy_table_test_sqlite.rb b/activerecord/test/cases/copy_table_test_sqlite.rb index f0cfb67866..72bd7e2dab 100644 --- a/activerecord/test/cases/copy_table_test_sqlite.rb +++ b/activerecord/test/cases/copy_table_test_sqlite.rb @@ -46,6 +46,17 @@ class CopyTableTest < ActiveRecord::TestCase test_copy_table('developers_projects', 'programmers_projects') end + def test_copy_table_with_id_col_that_is_not_primary_key + test_copy_table('goofy_string_id', 'goofy_string_id2') do |from, to, options| + original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' } + copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' } + assert_equal original_id.type, copied_id.type + assert_equal original_id.sql_type, copied_id.sql_type + assert_equal original_id.limit, copied_id.limit + assert_equal original_id.primary, copied_id.primary + end + end + protected def copy_table(from, to, options = {}) @connection.copy_table(from, to, {:temporary => true}.merge(options)) diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 80a06116ad..71e2ce8790 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -186,6 +186,16 @@ class MethodScopingTest < ActiveRecord::TestCase assert_equal authors(:david).attributes, scoped_authors.first.attributes end + def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_string_joins + scoped_authors = Author.with_scope(:find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do + Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN posts ON posts.author_id = authors.id'], :conditions => 'posts.id = 1') + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end + def test_scoped_count_include # with the include, will retrieve only developers for the given project Developer.with_scope(:find => { :include => :projects }) do diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index bab842cf66..e1e27fa130 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -277,6 +277,26 @@ class NamedScopeTest < ActiveRecord::TestCase post = Post.find(1) assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size end + + def test_chanining_should_use_latest_conditions_when_creating + post1 = Topic.rejected.approved.new + assert post1.approved? + + post2 = Topic.approved.rejected.new + assert ! post2.approved? + end + + def test_chanining_should_use_latest_conditions_when_searching + # Normal hash conditions + assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all + assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all + + # Nested hash conditions with same keys + assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all + + # Nested hash conditions with different keys + assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq + end end class DynamicScopeMatchTest < ActiveRecord::TestCase diff --git a/activerecord/test/connections/jdbc_jdbcderby/connection.rb b/activerecord/test/connections/jdbc_jdbcderby/connection.rb new file mode 100644 index 0000000000..222ef5db38 --- /dev/null +++ b/activerecord/test/connections/jdbc_jdbcderby/connection.rb @@ -0,0 +1,18 @@ +print "Using Derby via JRuby, activerecord-jdbc-adapter and activerecord-jdbcderby-adapter\n" +require_dependency 'models/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +ActiveRecord::Base.configurations = { + 'arunit' => { + :adapter => 'jdbcderby', + :database => 'activerecord_unittest' + }, + 'arunit2' => { + :adapter => 'jdbcderby', + :database => 'activerecord_unittest2' + } +} + +ActiveRecord::Base.establish_connection 'arunit' +Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/jdbc_jdbch2/connection.rb b/activerecord/test/connections/jdbc_jdbch2/connection.rb new file mode 100644 index 0000000000..9d2875e8e7 --- /dev/null +++ b/activerecord/test/connections/jdbc_jdbch2/connection.rb @@ -0,0 +1,18 @@ +print "Using H2 via JRuby, activerecord-jdbc-adapter and activerecord-jdbch2-adapter\n" +require_dependency 'models/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +ActiveRecord::Base.configurations = { + 'arunit' => { + :adapter => 'jdbch2', + :database => 'activerecord_unittest' + }, + 'arunit2' => { + :adapter => 'jdbch2', + :database => 'activerecord_unittest2' + } +} + +ActiveRecord::Base.establish_connection 'arunit' +Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb b/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb new file mode 100644 index 0000000000..fa943c2c76 --- /dev/null +++ b/activerecord/test/connections/jdbc_jdbchsqldb/connection.rb @@ -0,0 +1,18 @@ +print "Using HSQLDB via JRuby, activerecord-jdbc-adapter and activerecord-jdbchsqldb-adapter\n" +require_dependency 'models/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +ActiveRecord::Base.configurations = { + 'arunit' => { + :adapter => 'jdbchsqldb', + :database => 'activerecord_unittest' + }, + 'arunit2' => { + :adapter => 'jdbchsqldb', + :database => 'activerecord_unittest2' + } +} + +ActiveRecord::Base.establish_connection 'arunit' +Course.establish_connection 'arunit2' diff --git a/activerecord/test/connections/jdbc_jdbcmysql/connection.rb b/activerecord/test/connections/jdbc_jdbcmysql/connection.rb new file mode 100644 index 0000000000..e2517a50eb --- /dev/null +++ b/activerecord/test/connections/jdbc_jdbcmysql/connection.rb @@ -0,0 +1,26 @@ +print "Using MySQL via JRuby, activerecord-jdbc-adapter and activerecord-jdbcmysql-adapter\n" +require_dependency 'models/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost'; +# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost'; + +ActiveRecord::Base.configurations = { + 'arunit' => { + :adapter => 'jdbcmysql', + :username => 'rails', + :encoding => 'utf8', + :database => 'activerecord_unittest', + }, + 'arunit2' => { + :adapter => 'jdbcmysql', + :username => 'rails', + :database => 'activerecord_unittest2' + } +} + +ActiveRecord::Base.establish_connection 'arunit' +Course.establish_connection 'arunit2' + diff --git a/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb b/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb new file mode 100644 index 0000000000..0685da4433 --- /dev/null +++ b/activerecord/test/connections/jdbc_jdbcpostgresql/connection.rb @@ -0,0 +1,26 @@ +print "Using Postgrsql via JRuby, activerecord-jdbc-adapter and activerecord-postgresql-adapter\n" +require_dependency 'models/course' +require 'logger' + +ActiveRecord::Base.logger = Logger.new("debug.log") + +# createuser rails --createdb --no-superuser --no-createrole +# createdb -O rails activerecord_unittest +# createdb -O rails activerecord_unittest2 + +ActiveRecord::Base.configurations = { + 'arunit' => { + :adapter => 'jdbcpostgresql', + :username => ENV['USER'] || 'rails', + :database => 'activerecord_unittest' + }, + 'arunit2' => { + :adapter => 'jdbcpostgresql', + :username => ENV['USER'] || 'rails', + :database => 'activerecord_unittest2' + } +} + +ActiveRecord::Base.establish_connection 'arunit' +Course.establish_connection 'arunit2' + diff --git a/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb b/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb new file mode 100644 index 0000000000..26d4676ff3 --- /dev/null +++ b/activerecord/test/connections/jdbc_jdbcsqlite3/connection.rb @@ -0,0 +1,25 @@ +print "Using SQLite3 via JRuby, activerecord-jdbc-adapter and activerecord-jdbcsqlite3-adapter\n" +require_dependency 'models/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +class SqliteError < StandardError +end + +BASE_DIR = FIXTURES_ROOT +sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3" +sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" + +def make_connection(clazz, db_file) + ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'jdbcsqlite3', :database => db_file, :timeout => 5000 } } + unless File.exist?(db_file) + puts "SQLite3 database not found at #{db_file}. Rebuilding it." + sqlite_command = %Q{sqlite3 "#{db_file}" "create table a (a integer); drop table a;"} + puts "Executing '#{sqlite_command}'" + raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command) + end + clazz.establish_connection(clazz.name) +end + +make_connection(ActiveRecord::Base, sqlite_test_db) +make_connection(Course, sqlite_test_db2) diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index e0d8be676a..388fff8fba 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -17,6 +17,12 @@ class Post < ActiveRecord::Base has_one :last_comment, :class_name => 'Comment', :order => 'id desc' + named_scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } + named_scope :with_very_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'VerySpecialComment'} } + named_scope :with_post, lambda {|post_id| + { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } + } + has_many :comments, :order => "body" do def find_most_recent find(:first, :order => "id DESC") diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 39ca1bf42a..08bb24ed03 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -4,6 +4,8 @@ class Topic < ActiveRecord::Base { :conditions => ['written_on < ?', time] } } named_scope :approved, :conditions => {:approved => true} + named_scope :rejected, :conditions => {:approved => false} + named_scope :by_lifo, :conditions => {:author_name => 'lifo'} named_scope :approved_as_hash_condition, :conditions => {:topics => {:approved => true}} diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 094932d375..d44faf04cc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -154,6 +154,11 @@ ActiveRecord::Schema.define do t.string :name end + create_table :goofy_string_id, :force => true, :id => false do |t| + t.string :id, :null => false + t.string :info + end + create_table :items, :force => true do |t| t.column :name, :integer end diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 4192fab525..94418fb559 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -746,8 +746,8 @@ module ActiveResource # # => true # def ==(other) - other.equal?(self) || (other.instance_of?(self.class) && !other.new? && other.id == id) - end + other.equal?(self) || (other.instance_of?(self.class) && other.id == id && other.prefix_options == prefix_options) + end # Tests for equality (delegates to ==). def eql?(other) @@ -1006,7 +1006,7 @@ module ActiveResource # Takes a response from a typical create post and pulls the ID out def id_from_response(response) - response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] + response['Location'][/\/([^\/]*?)(\.\w+)?$/, 1] if response['Location'] end def element_path(options = nil) diff --git a/activeresource/test/base/equality_test.rb b/activeresource/test/base/equality_test.rb index 1fb8938e95..84f1a7b998 100644 --- a/activeresource/test/base/equality_test.rb +++ b/activeresource/test/base/equality_test.rb @@ -40,4 +40,13 @@ class BaseEqualityTest < Test::Unit::TestCase assert_equal resource.id.hash, resource.hash end end + + def test_with_prefix_options + assert_equal @one == @one, @one.eql?(@one) + assert_equal @one == @one.dup, @one.eql?(@one.dup) + new_one = @one.dup + new_one.prefix_options = {:foo => 'bar'} + assert_not_equal @one, new_one + end + end diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb index d37a6d4ed2..e22388f4a7 100644 --- a/activeresource/test/base_test.rb +++ b/activeresource/test/base_test.rb @@ -627,6 +627,12 @@ class BaseTest < Test::Unit::TestCase assert_equal '1', p.__send__(:id_from_response, resp) end + def test_id_from_response_without_location + p = Person.new + resp = {} + assert_equal nil, p.__send__(:id_from_response, resp) + end + def test_create_with_custom_prefix matzs_house = StreetAddress.new(:person_id => 1) matzs_house.save @@ -670,7 +676,6 @@ class BaseTest < Test::Unit::TestCase assert_equal person, person.reload end - def test_create rick = Person.create(:name => 'Rick') assert rick.valid? @@ -687,6 +692,14 @@ class BaseTest < Test::Unit::TestCase assert_raises(ActiveResource::ResourceConflict) { Person.create(:name => 'Rick') } end + def test_create_without_location + ActiveResource::HttpMock.respond_to do |mock| + mock.post "/people.xml", {}, nil, 201 + end + person = Person.create(:name => 'Rick') + assert_equal nil, person.id + end + def test_clone matz = Person.find(1) matz_c = matz.clone diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 621358d701..d83e259a2a 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -41,7 +41,7 @@ module ActiveSupport value else # forcing the value to be immutable - value.dup + value.duplicable? ? value.dup : value end end diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 25ea505813..66aab9e562 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -92,6 +92,10 @@ module ActiveSupport dup.merge!(other_hash) end + def inspect + "#<OrderedHash #{self.to_hash.inspect}>" + end + private def sync_keys! diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb index b5ad094d0e..76361bed90 100755 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb @@ -45,6 +45,11 @@ module I18n Thread.current[:locale] = locale end + # Returns an array of locales for which translations are available + def available_locales + backend.available_locales + end + # Sets the exception handler. def exception_handler=(exception_handler) @@exception_handler = exception_handler diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb index d298b3a85a..b54164d496 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb @@ -69,6 +69,12 @@ module I18n @initialized ||= false end + # Returns an array of locales for which translations are available + def available_locales + init_translations unless initialized? + translations.keys + end + def reload! @initialized = false @translations = nil @@ -76,7 +82,7 @@ module I18n protected def init_translations - load_translations(*I18n.load_path) + load_translations(*I18n.load_path.flatten) @initialized = true end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb index e181975f38..8ba7036abf 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb @@ -124,6 +124,16 @@ class I18nSimpleBackendTranslationsTest < Test::Unit::TestCase end end +class I18nSimpleBackendAvailableLocalesTest < Test::Unit::TestCase + def test_available_locales + @backend = I18n::Backend::Simple.new + @backend.store_translations 'de', :foo => 'bar' + @backend.store_translations 'en', :foo => 'foo' + + assert_equal ['de', 'en'], @backend.available_locales.map{|locale| locale.to_s }.sort + end +end + class I18nSimpleBackendTranslateTest < Test::Unit::TestCase include I18nSimpleBackendTestSetup @@ -472,6 +482,18 @@ class I18nSimpleBackendLoadTranslationsTest < Test::Unit::TestCase end end +class I18nSimpleBackendLoadPathTest < Test::Unit::TestCase + include I18nSimpleBackendTestSetup + + def test_nested_load_paths_do_not_break_locale_loading + @backend = I18n::Backend::Simple.new + I18n.load_path = [[File.dirname(__FILE__) + '/locale/en.yml']] + assert_nil backend_get_translations + assert_nothing_raised { @backend.send :init_translations } + assert_not_nil backend_get_translations + end +end + class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase include I18nSimpleBackendTestSetup diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index e8e0b41d4d..4e212f1661 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -81,6 +81,11 @@ module CacheStoreBehavior assert_equal({:a => "b"}, @cache.read('foo')) end + def test_should_read_and_write_integer + @cache.write('foo', 1) + assert_equal 1, @cache.read('foo') + end + def test_should_read_and_write_nil @cache.write('foo', nil) assert_equal nil, @cache.read('foo') @@ -200,6 +205,13 @@ uses_memcached 'memcached backed store' do end end + def test_local_cache_should_read_and_write_integer + @cache.with_local_cache do + @cache.write('foo', 1) + assert_equal 1, @cache.read('foo') + end + end + def test_local_cache_of_delete @cache.with_local_cache do @cache.write('foo', 'bar') diff --git a/ci/geminstaller.yml b/ci/geminstaller.yml index 4251518999..d11bf04485 100644 --- a/ci/geminstaller.yml +++ b/ci/geminstaller.yml @@ -11,8 +11,8 @@ gems: - name: mysql #version: >= 2.7 version: = 2.7 -- name: postgres - version: >= 0.7.9.2008.01.28 +- name: pg + version: >= 0.7.9.2008.10.13 - name: rack version: '~> 0.9.0' - name: rake diff --git a/railties/CHANGELOG b/railties/CHANGELOG index b36f57f75d..5b932ac197 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,8 +1,8 @@ *2.3.0 [Edge]* -* Remove script/performance/profiler in favour of performance integration tests. [Pratik Naik] +* Remove script/performance/request in favour of the performance integration tests. [Pratik Naik] - To continue using script/performance/profiler, install the request_profiler plugin : + To continue using script/performance/request, install the request_profiler plugin : script/plugin install git://github.com/rails/request_profiler.git diff --git a/railties/lib/commands/dbconsole.rb b/railties/lib/commands/dbconsole.rb index 06848d3c91..8002264f7e 100644 --- a/railties/lib/commands/dbconsole.rb +++ b/railties/lib/commands/dbconsole.rb @@ -3,12 +3,23 @@ require 'yaml' require 'optparse' include_password = false +options = {} OptionParser.new do |opt| opt.banner = "Usage: dbconsole [options] [environment]" opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| include_password = true end + + opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], + "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| + options['mode'] = mode + end + + opt.on("-h", "--header") do |h| + options['header'] = h + end + opt.parse!(ARGV) abort opt.to_s unless (0..1).include?(ARGV.size) end @@ -60,8 +71,13 @@ when "sqlite" exec(find_cmd('sqlite'), config["database"]) when "sqlite3" - exec(find_cmd('sqlite3'), config["database"]) + args = [] + + args << "-#{options['mode']}" if options['mode'] + args << "-header" if options['header'] + args << config['database'] + exec(find_cmd('sqlite3'), *args) else abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!" end diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index f6b8899d58..da064c86aa 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -302,7 +302,7 @@ module Rails if unloaded_gems.size > 0 @gems_dependencies_loaded = false # don't print if the gems rake tasks are being run - unless $rails_gem_installer + unless $rails_rake_task abort <<-end_error Missing these required gems: #{unloaded_gems.map { |gem| "#{gem.name} #{gem.requirement}" } * "\n "} @@ -379,6 +379,7 @@ Run `rake gems:install` to install the missing gems. # Eager load application classes def load_application_classes + return if $rails_rake_task if configuration.cache_classes configuration.eager_load_paths.each do |load_path| matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/ @@ -408,6 +409,7 @@ Run `rake gems:install` to install the missing gems. if configuration.frameworks.include?(:active_record) ActiveRecord::Base.configurations = configuration.database_configuration ActiveRecord::Base.establish_connection + configuration.middleware.use ActiveRecord::QueryCache end end @@ -542,7 +544,9 @@ Run `rake gems:install` to install the missing gems. end def initialize_metal - configuration.middleware.insert_before(:"ActionController::RewindableInput", Rails::Rack::Metal) + configuration.middleware.insert_before( + :"ActionController::RewindableInput", + Rails::Rack::Metal, :if => Rails::Rack::Metal.metals.any?) end # Initializes framework-specific settings for each of the loaded frameworks diff --git a/railties/lib/tasks/gems.rake b/railties/lib/tasks/gems.rake index 754e3ba5c9..e6731ab78c 100644 --- a/railties/lib/tasks/gems.rake +++ b/railties/lib/tasks/gems.rake @@ -17,13 +17,13 @@ end namespace :gems do task :base do - $rails_gem_installer = true + $rails_rake_task = true Rake::Task[:environment].invoke end desc "Build any native extensions for unpacked gems" task :build do - $rails_gem_installer = true + $rails_rake_task = true require 'rails/gem_builder' Dir[File.join(Rails::GemDependency.unpacked_path, '*')].each do |gem_dir| spec_file = File.join(gem_dir, '.specification') diff --git a/railties/lib/tasks/misc.rake b/railties/lib/tasks/misc.rake index 411750bf40..9e6f96db5b 100644 --- a/railties/lib/tasks/misc.rake +++ b/railties/lib/tasks/misc.rake @@ -1,5 +1,6 @@ task :default => :test task :environment do + $rails_rake_task = true require(File.join(RAILS_ROOT, 'config', 'environment')) end |