diff options
Diffstat (limited to 'actionpack/lib')
-rw-r--r-- | actionpack/lib/action_controller/base.rb | 9 | ||||
-rw-r--r-- | actionpack/lib/action_controller/caching/sweeping.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_controller/http_authentication.rb | 196 | ||||
-rw-r--r-- | actionpack/lib/action_controller/layout.rb | 2 | ||||
-rwxr-xr-x | actionpack/lib/action_controller/request.rb | 42 | ||||
-rw-r--r-- | actionpack/lib/action_controller/rewindable_input.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_controller/session/abstract_store.rb | 6 | ||||
-rw-r--r-- | actionpack/lib/action_controller/session/cookie_store.rb | 6 | ||||
-rw-r--r-- | actionpack/lib/action_controller/test_process.rb | 67 | ||||
-rw-r--r-- | actionpack/lib/action_controller/url_rewriter.rb | 9 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/form_options_helper.rb | 59 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/number_helper.rb | 45 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/text_helper.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_view/locale/en.yml | 13 | ||||
-rw-r--r-- | actionpack/lib/action_view/paths.rb | 9 | ||||
-rw-r--r-- | actionpack/lib/action_view/template.rb | 56 |
16 files changed, 401 insertions, 128 deletions
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 |