diff options
Diffstat (limited to 'actionpack/lib')
-rw-r--r-- | actionpack/lib/action_controller/metal/helpers.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/response.rb | 35 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/cookies.rb | 54 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/session/abstract_store.rb | 122 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/session/cookie_store.rb | 184 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb | 4 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/railtie.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/routing/mapper.rb | 5 | ||||
-rw-r--r-- | actionpack/lib/action_view.rb | 6 | ||||
-rw-r--r-- | actionpack/lib/action_view/base.rb | 3 | ||||
-rw-r--r-- | actionpack/lib/action_view/helpers/number_helper.rb | 7 | ||||
-rw-r--r-- | actionpack/lib/action_view/template.rb | 195 | ||||
-rw-r--r-- | actionpack/lib/action_view/template/error.rb | 18 | ||||
-rw-r--r-- | actionpack/lib/action_view/template/handlers/erb.rb | 139 | ||||
-rw-r--r-- | actionpack/lib/action_view/template/resolver.rb | 5 |
15 files changed, 414 insertions, 367 deletions
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 1110d7c117..b6d4fb1284 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -104,7 +104,7 @@ module ActionController def all_application_helpers helpers = [] helpers_path.each do |path| - extract = /^#{Regexp.quote(path)}\/?(.*)_helper.rb$/ + extract = /^#{Regexp.quote(path.to_s)}\/?(.*)_helper.rb$/ helpers += Dir["#{path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end helpers.sort! diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 8b730a97ee..3b85a98576 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -140,7 +140,7 @@ module ActionDispatch # :nodoc: def to_a assign_default_content_type_and_charset! handle_conditional_get! - self["Set-Cookie"] = @cookie.join("\n") unless @cookie.blank? + self["Set-Cookie"] = self["Set-Cookie"].join("\n") if self["Set-Cookie"].respond_to?(:join) self["ETag"] = @_etag if @_etag super end @@ -170,7 +170,7 @@ module ActionDispatch # :nodoc: # assert_equal 'AuthorOfNewPage', r.cookies['author'] def cookies cookies = {} - if header = @cookie + if header = self["Set-Cookie"] header = header.split("\n") if header.respond_to?(:to_str) header.each do |cookie| if pair = cookie.split(';').first @@ -182,37 +182,6 @@ module ActionDispatch # :nodoc: cookies end - def set_cookie(key, value) - case value - when Hash - domain = "; domain=" + value[:domain] if value[:domain] - path = "; path=" + value[:path] if value[:path] - # According to RFC 2109, we need dashes here. - # N.B.: cgi.rb uses spaces... - expires = "; expires=" + value[:expires].clone.gmtime. - strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] - secure = "; secure" if value[:secure] - httponly = "; HttpOnly" if value[:httponly] - value = value[:value] - end - value = [value] unless Array === value - cookie = Rack::Utils.escape(key) + "=" + - value.map { |v| Rack::Utils.escape v }.join("&") + - "#{domain}#{path}#{expires}#{secure}#{httponly}" - - @cookie << cookie - end - - def delete_cookie(key, value={}) - @cookie.reject! { |cookie| - cookie =~ /\A#{Rack::Utils.escape(key)}=/ - } - - set_cookie(key, - {:value => '', :path => nil, :domain => nil, - :expires => Time.at(0) }.merge(value)) - end - private def assign_default_content_type_and_charset! return if headers[CONTENT_TYPE].present? diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 42ab1d1ebb..87e8dd5010 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -52,9 +52,15 @@ module ActionDispatch # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. class Cookies + HTTP_HEADER = "Set-Cookie".freeze + TOKEN_KEY = "action_dispatch.secret_token".freeze + + # Raised when storing more than 4K of session data. + class CookieOverflow < StandardError; end + class CookieJar < Hash #:nodoc: def self.build(request) - secret = request.env["action_dispatch.secret_token"] + secret = request.env[TOKEN_KEY] new(secret).tap do |hash| hash.update(request.cookies) end @@ -134,9 +140,9 @@ module ActionDispatch @signed ||= SignedCookieJar.new(self, @secret) end - def write(response) - @set_cookies.each { |k, v| response.set_cookie(k, v) } - @delete_cookies.each { |k, v| response.delete_cookie(k, v) } + def write(headers) + @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) } + @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end end @@ -166,8 +172,11 @@ module ActionDispatch end class SignedCookieJar < CookieJar #:nodoc: + MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes. + SECRET_MIN_LENGTH = 30 # Characters + def initialize(parent_jar, secret) - raise "You must set config.secret_token in your app's config" if secret.blank? + ensure_secret_secure(secret) @parent_jar = parent_jar @verifier = ActiveSupport::MessageVerifier.new(secret) end @@ -176,6 +185,8 @@ module ActionDispatch if signed_message = @parent_jar[name] @verifier.verify(signed_message) end + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil end def []=(key, options) @@ -186,12 +197,34 @@ module ActionDispatch options = { :value => @verifier.generate(options) } end + raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[key] = options end def method_missing(method, *arguments, &block) @parent_jar.send(method, *arguments, &block) end + + protected + + # To prevent users from using something insecure like "Password" we make sure that the + # secret they've provided is at least 30 characters in length. + def ensure_secret_secure(secret) + if secret.blank? + raise ArgumentError, "A secret is required to generate an " + + "integrity hash for cookie session data. Use " + + "config.secret_token = \"some secret phrase of at " + + "least #{SECRET_MIN_LENGTH} characters\"" + + "in config/application.rb" + end + + if secret.length < SECRET_MIN_LENGTH + raise ArgumentError, "Secret should be something secure, " + + "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " + + "provided, \"#{secret}\", is shorter than the minimum length " + + "of #{SECRET_MIN_LENGTH} characters" + end + end end def initialize(app) @@ -202,12 +235,13 @@ module ActionDispatch status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] - response = Rack::Response.new(body, status, headers) - cookie_jar.write(response) - response.to_a - else - [status, headers, body] + cookie_jar.write(headers) + if headers[HTTP_HEADER].respond_to?(:join) + headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + end end + + [status, headers, body] end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index dddedc832f..15493cd2eb 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -1,5 +1,6 @@ require 'rack/utils' require 'rack/request' +require 'action_dispatch/middleware/cookies' require 'active_support/core_ext/object/blank' module ActionDispatch @@ -11,9 +12,6 @@ module ActionDispatch ENV_SESSION_KEY = 'rack.session'.freeze ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze - HTTP_COOKIE = 'HTTP_COOKIE'.freeze - SET_COOKIE = 'Set-Cookie'.freeze - class SessionHash < Hash def initialize(by, env) super() @@ -22,13 +20,6 @@ module ActionDispatch @loaded = false end - def session_id - ActiveSupport::Deprecation.warn( - "ActionDispatch::Session::AbstractStore::SessionHash#session_id " + - "has been deprecated. Please use request.session_options[:id] instead.", caller) - @env[ENV_SESSION_OPTIONS_KEY][:id] - end - def [](key) load! unless @loaded super(key.to_s) @@ -45,35 +36,14 @@ module ActionDispatch h end - def update(hash = nil) - if hash.nil? - ActiveSupport::Deprecation.warn('use replace instead', caller) - replace({}) - else - load! unless @loaded - super(hash.stringify_keys) - end - end - - def delete(key = nil) - if key.nil? - ActiveSupport::Deprecation.warn('use clear instead', caller) - clear - else - load! unless @loaded - super(key.to_s) - end - end - - def data - ActiveSupport::Deprecation.warn( - "ActionDispatch::Session::AbstractStore::SessionHash#data " + - "has been deprecated. Please use #to_hash instead.", caller) - to_hash + def update(hash) + load! unless @loaded + super(hash.stringify_keys) end - def close - ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller) + def delete(key) + load! unless @loaded + super(key.to_s) end def inspect @@ -124,30 +94,15 @@ module ActionDispatch } def initialize(app, options = {}) - # Process legacy CGI options - options = options.symbolize_keys - if options.has_key?(:session_path) - options[:path] = options.delete(:session_path) - end - if options.has_key?(:session_key) - options[:key] = options.delete(:session_key) - end - if options.has_key?(:session_http_only) - options[:httponly] = options.delete(:session_http_only) - end - @app = app @default_options = DEFAULT_OPTIONS.merge(options) - @key = @default_options[:key] - @cookie_only = @default_options[:cookie_only] + @key = @default_options.delete(:key).freeze + @cookie_only = @default_options.delete(:cookie_only) + ensure_session_key! end def call(env) - session = SessionHash.new(self, env) - - env[ENV_SESSION_KEY] = session - env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup - + prepare!(env) response = @app.call(env) session_data = env[ENV_SESSION_KEY] @@ -157,53 +112,62 @@ module ActionDispatch session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?) sid = options[:id] || generate_sid + session_data = session_data.to_hash - unless set_session(env, sid, session_data.to_hash) - return response - end + value = set_session(env, sid, session_data) + return response unless value - cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid) - cookie << "; domain=#{options[:domain]}" if options[:domain] - cookie << "; path=#{options[:path]}" if options[:path] - if options[:expire_after] - expiry = Time.now + options[:expire_after] - cookie << "; expires=#{expiry.httpdate}" - end - cookie << "; Secure" if options[:secure] - cookie << "; HttpOnly" if options[:httponly] - - headers = response[1] - unless headers[SET_COOKIE].blank? - headers[SET_COOKIE] << "\n#{cookie}" - else - headers[SET_COOKIE] = cookie + cookie = { :value => value } + unless options[:expire_after].nil? + cookie[:expires] = Time.now + options.delete(:expire_after) end + + request = ActionDispatch::Request.new(env) + set_cookie(request, cookie.merge!(options)) end response end private + + def prepare!(env) + env[ENV_SESSION_KEY] = SessionHash.new(self, env) + env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup + end + def generate_sid ActiveSupport::SecureRandom.hex(16) end + def set_cookie(request, options) + request.cookie_jar[@key] = options + end + def load_session(env) request = Rack::Request.new(env) - sid = request.cookies[@key] - unless @cookie_only - sid ||= request.params[@key] - end + sid = request.cookies[@key] + sid ||= request.params[@key] unless @cookie_only sid, session = get_session(env, sid) [sid, session] end + def ensure_session_key! + if @key.blank? + raise ArgumentError, 'A key is required to write a ' + + 'cookie containing the session data. Use ' + + 'config.session_store SESSION_STORE, { :key => ' + + '"_myapp_session" } in config/application.rb' + end + end + def get_session(env, sid) raise '#get_session needs to be implemented.' end def set_session(env, sid, session_data) - raise '#set_session needs to be implemented.' + raise '#set_session needs to be implemented and should return ' << + 'the value to be stored in the cookie (usually the sid)' end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 7114f42003..92a86ee229 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -38,23 +38,11 @@ module ActionDispatch # "rake secret" and set the key in config/environment.rb. # # Note that changing digest or secret invalidates all existing sessions! - class CookieStore - # Cookies can typically store 4096 bytes. - MAX = 4096 - SECRET_MIN_LENGTH = 30 # characters - - DEFAULT_OPTIONS = { - :key => '_session_id', - :domain => nil, - :path => "/", - :expire_after => nil, - :httponly => true - }.freeze - + class CookieStore < AbstractStore class OptionsHash < Hash def initialize(by, env, default_options) - @session_data = env[CookieStore::ENV_SESSION_KEY] - default_options.each { |key, value| self[key] = value } + @session_data = env[AbstractStore::ENV_SESSION_KEY] + merge!(default_options) end def [](key) @@ -62,172 +50,38 @@ module ActionDispatch end end - ENV_SESSION_KEY = "rack.session".freeze - ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze - HTTP_SET_COOKIE = "Set-Cookie".freeze - - # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end - def initialize(app, options = {}) - # Process legacy CGI options - options = options.symbolize_keys - if options.has_key?(:session_path) - options[:path] = options.delete(:session_path) - end - if options.has_key?(:session_key) - options[:key] = options.delete(:session_key) - end - if options.has_key?(:session_http_only) - options[:httponly] = options.delete(:session_http_only) - end - - @app = app - - # The session_key option is required. - ensure_session_key(options[:key]) - @key = options.delete(:key).freeze - - # The secret option is required. - ensure_secret_secure(options[:secret]) - @secret = options.delete(:secret).freeze - - @digest = options.delete(:digest) || 'SHA1' - @verifier = verifier_for(@secret, @digest) - - @default_options = DEFAULT_OPTIONS.merge(options).freeze - + super(app, options.merge!(:cookie_only => true)) freeze end - def call(env) - env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) - - status, headers, body = @app.call(env) - - session_data = env[ENV_SESSION_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?) - session_data = marshal(session_data.to_hash) - - raise CookieOverflow if session_data.size > MAX - - cookie = Hash.new - cookie[:value] = session_data - unless options[:expire_after].nil? - cookie[:expires] = Time.now + options[:expire_after] - end - - cookie = build_cookie(@key, cookie.merge(options)) - unless headers[HTTP_SET_COOKIE].blank? - headers[HTTP_SET_COOKIE] << "\n#{cookie}" - else - headers[HTTP_SET_COOKIE] = cookie - end - end - - [status, headers, body] - end - private - # Should be in Rack::Utils soon - def build_cookie(key, value) - case value - when Hash - domain = "; domain=" + value[:domain] if value[:domain] - path = "; path=" + value[:path] if value[:path] - # According to RFC 2109, we need dashes here. - # N.B.: cgi.rb uses spaces... - expires = "; expires=" + value[:expires].clone.gmtime. - strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] - secure = "; secure" if value[:secure] - httponly = "; HttpOnly" if value[:httponly] - value = value[:value] - end - value = [value] unless Array === value - cookie = Rack::Utils.escape(key) + "=" + - value.map { |v| Rack::Utils.escape(v) }.join("&") + - "#{domain}#{path}#{expires}#{secure}#{httponly}" + + def prepare!(env) + env[ENV_SESSION_KEY] = SessionHash.new(self, env) + env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) end def load_session(env) - request = Rack::Request.new(env) - session_data = request.cookies[@key] - data = unmarshal(session_data) || persistent_session_id!({}) + request = ActionDispatch::Request.new(env) + data = request.cookie_jar.signed[@key] + data = persistent_session_id!(data) data.stringify_keys! [data["session_id"], data] end - # Marshal a session hash into safe cookie data. Include an integrity hash. - def marshal(session) - @verifier.generate(persistent_session_id!(session)) - end - - # Unmarshal cookie data to a hash and verify its integrity. - def unmarshal(cookie) - persistent_session_id!(@verifier.verify(cookie)) if cookie - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil - end - - def ensure_session_key(key) - if key.blank? - raise ArgumentError, 'A key is required to write a ' + - 'cookie containing the session data. Use ' + - 'config.session_store :cookie_store, { :key => ' + - '"_myapp_session" } in config/application.rb' - end - end - - # To prevent users from using something insecure like "Password" we make sure that the - # secret they've provided is at least 30 characters in length. - def ensure_secret_secure(secret) - # There's no way we can do this check if they've provided a proc for the - # secret. - return true if secret.is_a?(Proc) - - if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_token = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/application.rb" - end - - if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" - end - end - - def verifier_for(secret, digest) - key = secret.respond_to?(:call) ? secret.call : secret - ActiveSupport::MessageVerifier.new(key, digest) - end - - def generate_sid - ActiveSupport::SecureRandom.hex(16) - end - - def persistent_session_id!(data) - (data ||= {}).merge!(inject_persistent_session_id(data)) + def set_cookie(request, options) + request.cookie_jar.signed[@key] = options end - def inject_persistent_session_id(data) - requires_session_id?(data) ? { "session_id" => generate_sid } : {} + def set_session(env, sid, session_data) + persistent_session_id!(session_data, sid) end - def requires_session_id?(data) - if data - data.respond_to?(:key?) && !data.key?("session_id") - else - true - end + def persistent_session_id!(data, sid=nil) + data ||= {} + data["session_id"] ||= sid || generate_sid + data end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index be1d5a43a2..8df8f977e8 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -38,9 +38,9 @@ module ActionDispatch options = env['rack.session.options'] expiry = options[:expire_after] || 0 @pool.set(sid, session_data, expiry) - return true + sid rescue MemCache::MemCacheError, Errno::ECONNREFUSED - return false + false end end end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 004c254e55..38da44d7e7 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -10,8 +10,6 @@ module ActionDispatch # Prepare dispatcher callbacks and run 'prepare' callbacks initializer "action_dispatch.prepare_dispatcher" do |app| - # TODO: This used to say unless defined?(Dispatcher). Find out why and fix. - require 'rails/dispatcher' ActionDispatch::Callbacks.to_prepare { app.routes_reloader.reload_if_changed } end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 4b02c2deb3..8a8d21c434 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -693,6 +693,11 @@ module ActionDispatch super end + def root(options={}) + options[:on] ||= :collection if @scope[:scope_level] == :resources + super(options) + end + protected def parent_resource #:nodoc: @scope[:scope_level_resource] diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 5555217ee2..9f56cca869 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -51,13 +51,17 @@ module ActionView autoload :MissingTemplate, 'action_view/template/error' autoload :ActionViewError, 'action_view/template/error' - autoload :TemplateError, 'action_view/template/error' + autoload :EncodingError, 'action_view/template/error' + autoload :TemplateError, 'action_view/template/error' + autoload :WrongEncodingError, 'action_view/template/error' autoload :TemplateHandler, 'action_view/template' autoload :TemplateHandlers, 'action_view/template' end autoload :TestCase, 'action_view/test_case' + + ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' end require 'active_support/i18n' diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 4ac2ee52d6..735398d972 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -201,7 +201,7 @@ module ActionView #:nodoc: end def initialize(lookup_context = nil, assigns_for_first_render = {}, controller = nil, formats = nil) #:nodoc: - @config = nil + @config = nil @assigns = assigns_for_first_render.each { |key, value| instance_variable_set("@#{key}", value) } @helpers = self.class.helpers || Module.new @@ -212,6 +212,7 @@ module ActionView #:nodoc: @_config = ActiveSupport::InheritableOptions.new(controller.config) if controller && controller.respond_to?(:config) @_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } @_virtual_path = nil + @output_buffer = nil @lookup_context = lookup_context.is_a?(ActionView::LookupContext) ? lookup_context : ActionView::LookupContext.new(lookup_context) diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 01fecc0f23..fccf004adf 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -13,6 +13,9 @@ module ActionView # unchanged if can't be converted into a valid number. module NumberHelper + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :unit => "$", :separator => ".", :delimiter => ",", + :precision => 2, :significant => false, :strip_insignificant_zeros => false } + # Raised when argument +number+ param given to the helpers is invalid and # the option :raise is set to +true+. class InvalidNumberError < StandardError @@ -104,9 +107,9 @@ module ActionView defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(currency) - options = options.reverse_merge(defaults) + defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + options = defaults.merge!(options) unit = options.delete(:unit) format = options.delete(:format) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index a1a970e2d2..5d8ac6b115 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,12 +1,89 @@ -# encoding: utf-8 -# This is so that templates compiled in this file are UTF-8 require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/kernel/singleton_class' module ActionView class Template extend ActiveSupport::Autoload + # === Encodings in ActionView::Template + # + # ActionView::Template is one of a few sources of potential + # encoding issues in Rails. This is because the source for + # templates are usually read from disk, and Ruby (like most + # encoding-aware programming languages) assumes that the + # String retrieved through File IO is encoded in the + # <tt>default_external</tt> encoding. In Rails, the default + # <tt>default_external</tt> encoding is UTF-8. + # + # As a result, if a user saves their template as ISO-8859-1 + # (for instance, using a non-Unicode-aware text editor), + # and uses characters outside of the ASCII range, their + # users will see diamonds with question marks in them in + # the browser. + # + # To mitigate this problem, we use a few strategies: + # 1. If the source is not valid UTF-8, we raise an exception + # when the template is compiled to alert the user + # to the problem. + # 2. The user can specify the encoding using Ruby-style + # encoding comments in any template engine. If such + # a comment is supplied, Rails will apply that encoding + # to the resulting compiled source returned by the + # template handler. + # 3. In all cases, we transcode the resulting String to + # the <tt>default_internal</tt> encoding (which defaults + # to UTF-8). + # + # This means that other parts of Rails can always assume + # that templates are encoded in UTF-8, even if the original + # source of the template was not UTF-8. + # + # From a user's perspective, the easiest thing to do is + # to save your templates as UTF-8. If you do this, you + # do not need to do anything else for things to "just work". + # + # === Instructions for template handlers + # + # The easiest thing for you to do is to simply ignore + # encodings. Rails will hand you the template source + # as the default_internal (generally UTF-8), raising + # an exception for the user before sending the template + # to you if it could not determine the original encoding. + # + # For the greatest simplicity, you can support only + # UTF-8 as the <tt>default_internal</tt>. This means + # that from the perspective of your handler, the + # entire pipeline is just UTF-8. + # + # === Advanced: Handlers with alternate metadata sources + # + # If you want to provide an alternate mechanism for + # specifying encodings (like ERB does via <%# encoding: ... %>), + # you may indicate that you are willing to accept + # BINARY data by implementing <tt>self.accepts_binary?</tt> + # on your handler. + # + # If you do, Rails will not raise an exception if + # the template's encoding could not be determined, + # assuming that you have another mechanism for + # making the determination. + # + # In this case, make sure you return a String from + # your handler encoded in the default_internal. Since + # you are handling out-of-band metadata, you are + # also responsible for alerting the user to any + # problems with converting the user's data to + # the default_internal. + # + # To do so, simply raise the raise WrongEncodingError + # as follows: + # + # raise WrongEncodingError.new( + # problematic_string, + # expected_encoding + # ) + eager_autoload do autoload :Error autoload :Handler @@ -16,20 +93,22 @@ module ActionView extend Template::Handlers - attr_reader :source, :identifier, :handler, :virtual_path, :formats + attr_reader :source, :identifier, :handler, :virtual_path, :formats, + :original_encoding - Finalizer = proc do |method_name| + Finalizer = proc do |method_name, mod| proc do - ActionView::CompiledTemplates.module_eval do + mod.module_eval do remove_possible_method method_name end end end def initialize(source, identifier, handler, details) - @source = source - @identifier = identifier - @handler = handler + @source = source + @identifier = identifier + @handler = handler + @original_encoding = nil @virtual_path = details[:virtual_path] @method_names = {} @@ -42,7 +121,13 @@ module ActionView # Notice that we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do - method_name = compile(locals, view) + if view.is_a?(ActionView::CompiledTemplates) + mod = ActionView::CompiledTemplates + else + mod = view.singleton_class + end + + method_name = compile(locals, view, mod) view.send(method_name, locals, &block) end rescue Exception => e @@ -50,7 +135,7 @@ module ActionView e.sub_template_of(self) raise e else - raise Template::Error.new(self, view.assigns, e) + raise Template::Error.new(self, view.respond_to?(:assigns) ? view.assigns : {}, e) end end @@ -75,37 +160,97 @@ module ActionView end private - def compile(locals, view) + # Among other things, this method is responsible for properly setting + # the encoding of the source. Until this point, we assume that the + # source is BINARY data. If no additional information is supplied, + # we assume the encoding is the same as Encoding.default_external. + # + # The user can also specify the encoding via a comment on the first + # line of the template (# encoding: NAME-OF-ENCODING). This will work + # with any template engine, as we process out the encoding comment + # before passing the source on to the template engine, leaving a + # blank line in its stead. + # + # Note that after we figure out the correct encoding, we then + # encode the source into Encoding.default_internal. In general, + # this means that templates will be UTF-8 inside of Rails, + # regardless of the original source encoding. + def compile(locals, view, mod) method_name = build_method_name(locals) return method_name if view.respond_to?(method_name) locals_code = locals.keys.map! { |key| "#{key} = local_assigns[:#{key}];" }.join - code = @handler.call(self) - if code.sub!(/\A(#.*coding.*)\n/, '') - encoding_comment = $1 - elsif defined?(Encoding) && Encoding.respond_to?(:default_external) - encoding_comment = "#coding:#{Encoding.default_external}" + if source.encoding_aware? + if source.sub!(/\A#{ENCODING_FLAG}/, '') + encoding = $1 + else + encoding = Encoding.default_external + end + + # Tag the source with the default external encoding + # or the encoding specified in the file + source.force_encoding(encoding) + + # If the original encoding is BINARY, the actual + # encoding is either stored out-of-band (such as + # in ERB <%# %> style magic comments) or missing. + # This is also true if the original encoding is + # something other than BINARY, but it's invalid. + if source.encoding != Encoding::BINARY && source.valid_encoding? + source.encode! + # If the assumed encoding is incorrect, check to + # see whether the handler accepts BINARY. If it + # does, it has another mechanism for determining + # the true encoding of the String. + elsif @handler.respond_to?(:accepts_binary?) && @handler.accepts_binary? + source.force_encoding(Encoding::BINARY) + # If the handler does not accept BINARY, the + # assumed encoding (either the default_external, + # or the explicit encoding specified by the user) + # is incorrect. We raise an exception here. + else + raise WrongEncodingError.new(source, encoding) + end + + # Don't validate the encoding yet -- the handler + # may treat the String as raw bytes and extract + # the encoding some other way end + code = @handler.call(self) + source = <<-end_src def #{method_name}(local_assigns) - _old_virtual_path, @_virtual_path = @_virtual_path, #{@virtual_path.inspect};_old_output_buffer = output_buffer;#{locals_code};#{code} + _old_virtual_path, @_virtual_path = @_virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} ensure - @_virtual_path, self.output_buffer = _old_virtual_path, _old_output_buffer + @_virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer end end_src - if encoding_comment - source = "#{encoding_comment}\n#{source}" - line = -1 - else - line = 0 + if source.encoding_aware? + # Handlers should return their source Strings in either the + # default_internal or BINARY. If the handler returns a BINARY + # String, we assume its encoding is the one we determined + # earlier, and encode the resulting source in the default_internal. + if source.encoding == Encoding::BINARY + source.force_encoding(Encoding.default_internal) + end + + # In case we get back a String from a handler that is not in + # BINARY or the default_internal, encode it to the default_internal + source.encode! + + # Now, validate that the source we got back from the template + # handler is valid in the default_internal + unless source.valid_encoding? + raise WrongEncodingError.new(@source, Encoding.default_internal) + end end begin - ActionView::CompiledTemplates.module_eval(source, identifier, line) - ObjectSpace.define_finalizer(self, Finalizer[method_name]) + mod.module_eval(source, identifier, 0) + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) method_name rescue Exception => e # errors from template code diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index 6866eabf77..d3a53d2147 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -4,6 +4,24 @@ module ActionView class ActionViewError < StandardError #:nodoc: end + class EncodingError < StandardError #:nodoc: + end + + class WrongEncodingError < EncodingError #:nodoc: + def initialize(string, encoding) + @string, @encoding = string, encoding + end + + def message + "Your template was not saved as valid #{@encoding}. Please " \ + "either specify #{@encoding} as the encoding for your template " \ + "in your text editor, or mark the template with its " \ + "encoding by inserting the following as the first line " \ + "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \ + "The source of your template was:\n\n#{@string}" + end + end + class MissingTemplate < ActionViewError #:nodoc: attr_reader :path diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 237746437a..cbed0108cf 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,9 +1,15 @@ require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/string/output_safety' +require "action_view/template" require 'erubis' module ActionView class OutputBuffer < ActiveSupport::SafeBuffer + def initialize(*) + super + encode! if encoding_aware? + end + def <<(value) super(value.to_s) end @@ -17,65 +23,108 @@ module ActionView end end - module Template::Handlers - class Erubis < ::Erubis::Eruby - def add_preamble(src) - src << "@output_buffer = ActionView::OutputBuffer.new;" - end + class Template + module Handlers + class Erubis < ::Erubis::Eruby + def add_preamble(src) + src << "@output_buffer = ActionView::OutputBuffer.new;" + end - def add_text(src, text) - return if text.empty? - src << "@output_buffer.safe_concat('" << escape_text(text) << "');" - end + def add_text(src, text) + return if text.empty? + src << "@output_buffer.safe_concat('" << escape_text(text) << "');" + end - BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ + BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/ - def add_expr_literal(src, code) - if code =~ BLOCK_EXPR - src << '@output_buffer.append= ' << code - else - src << '@output_buffer.append= (' << code << ');' + def add_expr_literal(src, code) + if code =~ BLOCK_EXPR + src << '@output_buffer.append= ' << code + else + src << '@output_buffer.append= (' << code << ');' + end end - end - def add_stmt(src, code) - if code =~ BLOCK_EXPR - src << '@output_buffer.append_if_string= ' << code - else - super + def add_stmt(src, code) + if code =~ BLOCK_EXPR + src << '@output_buffer.append_if_string= ' << code + else + super + end end - end - def add_expr_escaped(src, code) - src << '@output_buffer.append= ' << escaped_expr(code) << ';' - end + def add_expr_escaped(src, code) + src << '@output_buffer.append= ' << escaped_expr(code) << ';' + end - def add_postamble(src) - src << '@output_buffer.to_s' + def add_postamble(src) + src << '@output_buffer.to_s' + end end - end - class ERB < Template::Handler - include Compilable + class ERB < Handler + include Compilable + + ## + # :singleton-method: + # Specify trim mode for the ERB compiler. Defaults to '-'. + # See ERb documentation for suitable values. + cattr_accessor :erb_trim_mode + self.erb_trim_mode = '-' + + self.default_format = Mime::HTML + + cattr_accessor :erb_implementation + self.erb_implementation = Erubis - ## - # :singleton-method: - # Specify trim mode for the ERB compiler. Defaults to '-'. - # See ERb documentation for suitable values. - cattr_accessor :erb_trim_mode - self.erb_trim_mode = '-' + ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") - self.default_format = Mime::HTML + def self.accepts_binary? + true + end + + def compile(template) + if template.source.encoding_aware? + # Even though Rails has given us a String tagged with the + # default_internal encoding (likely UTF-8), it is possible + # that the String is actually encoded using a different + # encoding, specified via an ERB magic comment. If the + # String is not actually UTF-8, the regular expression + # engine will (correctly) raise an exception. For now, + # we'll reset the String to BINARY so we can run regular + # expressions against it + template_source = template.source.dup.force_encoding("BINARY") + + # Erubis does not have direct support for encodings. + # As a result, we will extract the ERB-style magic + # comment, give the String to Erubis as BINARY data, + # and then tag the resulting String with the extracted + # encoding later + erb = template_source.gsub(ENCODING_TAG, '') + encoding = $2 - cattr_accessor :erb_implementation - self.erb_implementation = Erubis + if !encoding && (template.source.encoding == Encoding::BINARY) + raise WrongEncodingError.new(template_source, Encoding.default_external) + end + else + erb = template.source.dup + end - def compile(template) - source = template.source.gsub(/\A(<%(#.*coding[:=]\s*(\S+)\s*)-?%>)\s*\n?/, '') - erb = "<% __in_erb_template=true %>#{source}" - result = self.class.erb_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src - result = "#{$2}\n#{result}" if $2 - result + result = self.class.erb_implementation.new( + erb, + :trim => (self.class.erb_trim_mode == "-") + ).src + + # If an encoding tag was found, tag the String + # we're returning with that encoding. Otherwise, + # return a BINARY String, which is what ERB + # returns. Note that if a magic comment was + # not specified, we will return the data to + # Rails as BINARY, which will then use its + # own encoding logic to create a UTF-8 String. + result = "\n#{result}".force_encoding(encoding).encode if encoding + result + end end end end diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index a223b3a55f..ef44925951 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -70,7 +70,10 @@ module ActionView Dir[query].reject { |p| File.directory?(p) }.map do |p| handler, format = extract_handler_and_format(p, formats) - Template.new(File.read(p), File.expand_path(p), handler, + + contents = File.open(p, "rb") {|io| io.read } + + Template.new(contents, File.expand_path(p), handler, :virtual_path => path, :format => format) end end |