diff options
Diffstat (limited to 'actionpack')
34 files changed, 873 insertions, 202 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/base.rb b/actionpack/lib/action_controller/base.rb index 50b965ce4c..36b80d5780 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -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/request.rb b/actionpack/lib/action_controller/request.rb index f4485f6941..a8729bb6f5 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'] @@ -257,25 +255,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? @@ -295,14 +289,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 @@ -312,7 +304,6 @@ EOM standard_port end end - memoize :port # Returns the standard \port number for this request's protocol. def standard_port @@ -350,7 +341,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. @@ -376,7 +366,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. @@ -385,7 +374,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 6ad6369950..5a728d1877 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -93,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..4b5fc3a3c1 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -15,7 +15,7 @@ module ActionController #:nodoc: end def reset_session - @session = TestSession.new + @session.reset end # Wraps raw_post in a StringIO. @@ -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,62 @@ 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 + reset_session_id + replace_attributes(attributes) + end + + def reset + reset_session_id + replace_attributes({ }) 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 + + private + + def reset_session_id + @session_id = '' + end + + def replace_attributes(attributes = nil) + attributes ||= {} + replace(attributes.stringify_keys) end end 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_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 08a25bfdb8..fbc94a0df7 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -1,35 +1,35 @@ require 'abstract_unit' -class DummyController < ActionController::Base - before_filter :authenticate, :only => :index - before_filter :authenticate_with_request, :only => :display +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 index + render :text => "Hello Secret" + end - def display - render :text => 'Definitely Maybe' - end + def display + render :text => 'Definitely Maybe' + end - private + private - def authenticate - authenticate_or_request_with_http_basic do |username, password| - username == 'lifo' && password == 'world' + def authenticate + authenticate_or_request_with_http_basic do |username, password| + username == 'lifo' && password == 'world' + end 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") + 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 -end -class HttpBasicAuthenticationTest < ActionController::TestCase AUTH_HEADERS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION', 'REDIRECT_X_HTTP_AUTHORIZATION'] tests DummyController 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/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_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 b6a38f47aa..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 @@ -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/session/test_session_test.rb b/actionpack/test/controller/session/test_session_test.rb new file mode 100644 index 0000000000..83103be3ec --- /dev/null +++ b/actionpack/test/controller/session/test_session_test.rb @@ -0,0 +1,58 @@ +require 'abstract_unit' +require 'stringio' + +class ActionController::TestSessionTest < ActiveSupport::TestCase + + def test_calling_delete_without_parameters_raises_deprecation_warning_and_calls_to_clear_test_session + assert_deprecated(/use clear instead/){ ActionController::TestSession.new.delete } + end + + def test_calling_update_without_parameters_raises_deprecation_warning_and_calls_to_clear_test_session + assert_deprecated(/use replace instead/){ ActionController::TestSession.new.update } + end + + def test_calling_close_raises_deprecation_warning + assert_deprecated(/sessions should no longer be closed/){ ActionController::TestSession.new.close } + end + + def test_defaults + session = ActionController::TestSession.new + assert_equal({}, session.data) + assert_equal('', session.session_id) + end + + def test_ctor_allows_setting + session = ActionController::TestSession.new({:one => 'one', :two => 'two'}) + assert_equal('one', session[:one]) + assert_equal('two', session[:two]) + end + + def test_setting_session_item_sets_item + session = ActionController::TestSession.new + session[:key] = 'value' + assert_equal('value', session[:key]) + end + + def test_calling_delete_removes item + session = ActionController::TestSession.new + session[:key] = 'value' + assert_equal('value', session[:key]) + session.delete(:key) + assert_nil(session[:key]) + end + + def test_calling_update_with_params_passes_to_attributes + session = ActionController::TestSession.new() + session.update('key' => 'value') + assert_equal('value', session[:key]) + end + + def test_clear_emptys_session + params = {:one => 'one', :two => 'two'} + session = ActionController::TestSession.new({:one => 'one', :two => 'two'}) + session.clear + assert_nil(session[:one]) + assert_nil(session[:two]) + end + +end
\ No newline at end of file diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index ee7b8ade8c..65c894c2e7 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -23,6 +23,11 @@ class TestTest < ActionController::TestCase render :text => 'Success' end + def reset_the_session + reset_session + render :text => 'ignore me' + end + def render_raw_post raise ActiveSupport::TestCase::Assertion, "#raw_post is blank" if request.raw_post.blank? render :text => request.raw_post @@ -171,6 +176,24 @@ XML assert_equal 'value2', session[:symbol] end + def test_session_is_cleared_from_controller_after_reset_session + process :set_session + process :reset_the_session + assert_equal Hash.new, @controller.session.to_hash + end + + def test_session_is_cleared_from_response_after_reset_session + process :set_session + process :reset_the_session + assert_equal Hash.new, @response.session.to_hash + end + + def test_session_is_cleared_from_request_after_reset_session + process :set_session + process :reset_the_session + assert_equal Hash.new, @request.session.to_hash + end + def test_process_with_request_uri_with_no_params process :test_uri assert_equal "/test_test/test/test_uri", @response.body 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/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)) |