1
- # encoding: utf-8
2
- # This is so that templates compiled in this file are UTF-8
3
1
require 'active_support/core_ext/array/wrap'
4
2
require 'active_support/core_ext/object/blank'
3
+ require 'active_support/core_ext/kernel/singleton_class'
5
4
6
5
module ActionView
7
6
class Template
8
7
extend ActiveSupport ::Autoload
9
8
9
+ # === Encodings in ActionView::Template
10
+ #
11
+ # ActionView::Template is one of a few sources of potential
12
+ # encoding issues in Rails. This is because the source for
13
+ # templates are usually read from disk, and Ruby (like most
14
+ # encoding-aware programming languages) assumes that the
15
+ # String retrieved through File IO is encoded in the
16
+ # <tt>default_external</tt> encoding. In Rails, the default
17
+ # <tt>default_external</tt> encoding is UTF-8.
18
+ #
19
+ # As a result, if a user saves their template as ISO-8859-1
20
+ # (for instance, using a non-Unicode-aware text editor),
21
+ # and uses characters outside of the ASCII range, their
22
+ # users will see diamonds with question marks in them in
23
+ # the browser.
24
+ #
25
+ # To mitigate this problem, we use a few strategies:
26
+ # 1. If the source is not valid UTF-8, we raise an exception
27
+ # when the template is compiled to alert the user
28
+ # to the problem.
29
+ # 2. The user can specify the encoding using Ruby-style
30
+ # encoding comments in any template engine. If such
31
+ # a comment is supplied, Rails will apply that encoding
32
+ # to the resulting compiled source returned by the
33
+ # template handler.
34
+ # 3. In all cases, we transcode the resulting String to
35
+ # the <tt>default_internal</tt> encoding (which defaults
36
+ # to UTF-8).
37
+ #
38
+ # This means that other parts of Rails can always assume
39
+ # that templates are encoded in UTF-8, even if the original
40
+ # source of the template was not UTF-8.
41
+ #
42
+ # From a user's perspective, the easiest thing to do is
43
+ # to save your templates as UTF-8. If you do this, you
44
+ # do not need to do anything else for things to "just work".
45
+ #
46
+ # === Instructions for template handlers
47
+ #
48
+ # The easiest thing for you to do is to simply ignore
49
+ # encodings. Rails will hand you the template source
50
+ # as the default_internal (generally UTF-8), raising
51
+ # an exception for the user before sending the template
52
+ # to you if it could not determine the original encoding.
53
+ #
54
+ # For the greatest simplicity, you can support only
55
+ # UTF-8 as the <tt>default_internal</tt>. This means
56
+ # that from the perspective of your handler, the
57
+ # entire pipeline is just UTF-8.
58
+ #
59
+ # === Advanced: Handlers with alternate metadata sources
60
+ #
61
+ # If you want to provide an alternate mechanism for
62
+ # specifying encodings (like ERB does via <%# encoding: ... %>),
63
+ # you may indicate that you are willing to accept
64
+ # BINARY data by implementing <tt>self.accepts_binary?</tt>
65
+ # on your handler.
66
+ #
67
+ # If you do, Rails will not raise an exception if
68
+ # the template's encoding could not be determined,
69
+ # assuming that you have another mechanism for
70
+ # making the determination.
71
+ #
72
+ # In this case, make sure you return a String from
73
+ # your handler encoded in the default_internal. Since
74
+ # you are handling out-of-band metadata, you are
75
+ # also responsible for alerting the user to any
76
+ # problems with converting the user's data to
77
+ # the default_internal.
78
+ #
79
+ # To do so, simply raise the raise WrongEncodingError
80
+ # as follows:
81
+ #
82
+ # raise WrongEncodingError.new(
83
+ # problematic_string,
84
+ # expected_encoding
85
+ # )
86
+
10
87
eager_autoload do
11
88
autoload :Error
12
89
autoload :Handler
@@ -16,26 +93,22 @@ class Template
16
93
17
94
extend Template ::Handlers
18
95
19
- attr_reader :source , :identifier , :handler , :virtual_path , :formats
96
+ attr_reader :source , :identifier , :handler , :virtual_path , :formats ,
97
+ :original_encoding
20
98
21
- Finalizer = proc do |method_name |
99
+ Finalizer = proc do |method_name , mod |
22
100
proc do
23
- ActionView :: CompiledTemplates . module_eval do
101
+ mod . module_eval do
24
102
remove_possible_method method_name
25
103
end
26
104
end
27
105
end
28
106
29
107
def initialize ( source , identifier , handler , details )
30
- if source . encoding_aware? && source =~ %r{\A #{ ENCODING_FLAG } }
31
- # don't snip off the \n to preserve line numbers
32
- source . sub! ( /\A [^\n ]*/ , '' )
33
- source . force_encoding ( $1) . encode
34
- end
35
-
36
- @source = source
37
- @identifier = identifier
38
- @handler = handler
108
+ @source = source
109
+ @identifier = identifier
110
+ @handler = handler
111
+ @original_encoding = nil
39
112
40
113
@virtual_path = details [ :virtual_path ]
41
114
@method_names = { }
@@ -48,15 +121,21 @@ def render(view, locals, &block)
48
121
# Notice that we use a bang in this instrumentation because you don't want to
49
122
# consume this in production. This is only slow if it's being listened to.
50
123
ActiveSupport ::Notifications . instrument ( "!render_template.action_view" , :virtual_path => @virtual_path ) do
51
- method_name = compile ( locals , view )
124
+ if view . is_a? ( ActionView ::CompiledTemplates )
125
+ mod = ActionView ::CompiledTemplates
126
+ else
127
+ mod = view . singleton_class
128
+ end
129
+
130
+ method_name = compile ( locals , view , mod )
52
131
view . send ( method_name , locals , &block )
53
132
end
54
133
rescue Exception => e
55
134
if e . is_a? ( Template ::Error )
56
135
e . sub_template_of ( self )
57
136
raise e
58
137
else
59
- raise Template ::Error . new ( self , view . assigns , e )
138
+ raise Template ::Error . new ( self , view . respond_to? ( : assigns) ? view . assigns : { } , e )
60
139
end
61
140
end
62
141
@@ -81,37 +160,97 @@ def inspect
81
160
end
82
161
83
162
private
84
- def compile ( locals , view )
163
+ # Among other things, this method is responsible for properly setting
164
+ # the encoding of the source. Until this point, we assume that the
165
+ # source is BINARY data. If no additional information is supplied,
166
+ # we assume the encoding is the same as Encoding.default_external.
167
+ #
168
+ # The user can also specify the encoding via a comment on the first
169
+ # line of the template (# encoding: NAME-OF-ENCODING). This will work
170
+ # with any template engine, as we process out the encoding comment
171
+ # before passing the source on to the template engine, leaving a
172
+ # blank line in its stead.
173
+ #
174
+ # Note that after we figure out the correct encoding, we then
175
+ # encode the source into Encoding.default_internal. In general,
176
+ # this means that templates will be UTF-8 inside of Rails,
177
+ # regardless of the original source encoding.
178
+ def compile ( locals , view , mod )
85
179
method_name = build_method_name ( locals )
86
180
return method_name if view . respond_to? ( method_name )
87
181
88
182
locals_code = locals . keys . map! { |key | "#{ key } = local_assigns[:#{ key } ];" } . join
89
183
90
- code = @handler . call ( self )
91
- if code . sub! ( /\A (#.*coding.*)\n / , '' )
92
- encoding_comment = $1
93
- elsif defined? ( Encoding ) && Encoding . respond_to? ( :default_external )
94
- encoding_comment = "#coding:#{ Encoding . default_external } "
184
+ if source . encoding_aware?
185
+ if source . sub! ( /\A #{ ENCODING_FLAG } / , '' )
186
+ encoding = $1
187
+ else
188
+ encoding = Encoding . default_external
189
+ end
190
+
191
+ # Tag the source with the default external encoding
192
+ # or the encoding specified in the file
193
+ source . force_encoding ( encoding )
194
+
195
+ # If the original encoding is BINARY, the actual
196
+ # encoding is either stored out-of-band (such as
197
+ # in ERB <%# %> style magic comments) or missing.
198
+ # This is also true if the original encoding is
199
+ # something other than BINARY, but it's invalid.
200
+ if source . encoding != Encoding ::BINARY && source . valid_encoding?
201
+ source . encode!
202
+ # If the assumed encoding is incorrect, check to
203
+ # see whether the handler accepts BINARY. If it
204
+ # does, it has another mechanism for determining
205
+ # the true encoding of the String.
206
+ elsif @handler . respond_to? ( :accepts_binary? ) && @handler . accepts_binary?
207
+ source . force_encoding ( Encoding ::BINARY )
208
+ # If the handler does not accept BINARY, the
209
+ # assumed encoding (either the default_external,
210
+ # or the explicit encoding specified by the user)
211
+ # is incorrect. We raise an exception here.
212
+ else
213
+ raise WrongEncodingError . new ( source , encoding )
214
+ end
215
+
216
+ # Don't validate the encoding yet -- the handler
217
+ # may treat the String as raw bytes and extract
218
+ # the encoding some other way
95
219
end
96
220
221
+ code = @handler . call ( self )
222
+
97
223
source = <<-end_src
98
224
def #{ method_name } (local_assigns)
99
- _old_virtual_path, @_virtual_path = @_virtual_path, #{ @virtual_path . inspect } ;_old_output_buffer = output_buffer;#{ locals_code } ;#{ code }
225
+ _old_virtual_path, @_virtual_path = @_virtual_path, #{ @virtual_path . inspect } ;_old_output_buffer = @ output_buffer;#{ locals_code } ;#{ code }
100
226
ensure
101
- @_virtual_path, self. output_buffer = _old_virtual_path, _old_output_buffer
227
+ @_virtual_path, @ output_buffer = _old_virtual_path, _old_output_buffer
102
228
end
103
229
end_src
104
230
105
- if encoding_comment
106
- source = "#{ encoding_comment } \n #{ source } "
107
- line = -1
108
- else
109
- line = 0
231
+ if source . encoding_aware?
232
+ # Handlers should return their source Strings in either the
233
+ # default_internal or BINARY. If the handler returns a BINARY
234
+ # String, we assume its encoding is the one we determined
235
+ # earlier, and encode the resulting source in the default_internal.
236
+ if source . encoding == Encoding ::BINARY
237
+ source . force_encoding ( Encoding . default_internal )
238
+ end
239
+
240
+ # In case we get back a String from a handler that is not in
241
+ # BINARY or the default_internal, encode it to the default_internal
242
+ source . encode!
243
+
244
+ # Now, validate that the source we got back from the template
245
+ # handler is valid in the default_internal
246
+ unless source . valid_encoding?
247
+ raise WrongEncodingError . new ( @source , Encoding . default_internal )
248
+ end
110
249
end
111
250
112
251
begin
113
- ActionView :: CompiledTemplates . module_eval ( source , identifier , line )
114
- ObjectSpace . define_finalizer ( self , Finalizer [ method_name ] )
252
+ mod . module_eval ( source , identifier , 0 )
253
+ ObjectSpace . define_finalizer ( self , Finalizer [ method_name , mod ] )
115
254
116
255
method_name
117
256
rescue Exception => e # errors from template code
0 commit comments