template.rb 14.5 KB
Newer Older
1
# frozen_string_literal: true
2

3 4
require "active_support/core_ext/object/try"
require "active_support/core_ext/kernel/singleton_class"
5
require "active_support/deprecation"
6
require "thread"
7
require "delegate"
8

9
module ActionView
10
  # = Action View Template
11
  class Template
C
Carlhuda 已提交
12
    extend ActiveSupport::Autoload
J
Joshua Peek 已提交
13

14 15 16 17 18 19 20 21
    def self.finalize_compiled_template_methods
      ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect"
    end

    def self.finalize_compiled_template_methods=(_)
      ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect"
    end

22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
    # === 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.
    #
38 39 40 41
    # For the rest of this documentation, when we say "UTF-8",
    # we mean "UTF-8 or whatever the default_internal encoding
    # is set to". By default, it will be UTF-8.
    #
42 43 44 45 46 47 48 49 50 51
    # 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
52
    #    the UTF-8.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
    #
    # 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: ... %>),
79
    # you may indicate that you will handle encodings yourself
80
    # by implementing <tt>handles_encoding?</tt> on your handler.
81
    #
82 83 84 85
    # If you do, Rails will not try to encode the String
    # into the default_internal, passing you the unaltered
    # bytes tagged with the assumed encoding (from
    # default_external).
86 87 88 89 90 91
    #
    # 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
92
    # the <tt>default_internal</tt>.
93
    #
94
    # To do so, simply raise +WrongEncodingError+ as follows:
95 96 97 98 99 100
    #
    #     raise WrongEncodingError.new(
    #       problematic_string,
    #       expected_encoding
    #     )

101 102 103 104 105 106 107 108 109 110 111 112 113
    ##
    # :method: local_assigns
    #
    # Returns a hash with the defined local variables.
    #
    # Given this sub template rendering:
    #
    #   <%= render "shared/header", { headline: "Welcome", person: person } %>
    #
    # You can use +local_assigns+ in the sub templates to access the local variables:
    #
    #   local_assigns[:headline] # => "Welcome"

J
Joshua Peek 已提交
114 115 116
    eager_autoload do
      autoload :Error
      autoload :Handlers
117
      autoload :HTML
118
      autoload :Inline
J
Joshua Peek 已提交
119
      autoload :Text
120
      autoload :Types
J
Joshua Peek 已提交
121 122
    end

C
Carlhuda 已提交
123
    extend Template::Handlers
124

J
John Hawthorn 已提交
125
    attr_reader :source, :identifier, :handler, :original_encoding
126
    attr_reader :variable, :format, :variant, :locals, :virtual_path
127

J
John Hawthorn 已提交
128
    def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil)
129 130 131 132 133
      unless locals
        ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter"
        locals = []
      end

J
José Valim 已提交
134 135 136 137
      @source            = source
      @identifier        = identifier
      @handler           = handler
      @compiled          = false
138
      @locals            = locals
139
      @virtual_path      = virtual_path
140 141 142 143 144 145 146

      @variable = if @virtual_path
        base = @virtual_path[-1] == "/" ? "" : File.basename(@virtual_path)
        base =~ /\A_?(.*?)(?:\.\w+)*\z/
        $1.to_sym
      end

A
Aaron Patterson 已提交
147
      @format            = format
A
Aaron Patterson 已提交
148
      @variant           = variant
149
      @compile_mutex     = Mutex.new
150
    end
151

152
    deprecate :original_encoding
153
    deprecate def virtual_path=(_); end
154
    deprecate def locals=(_); end
A
Aaron Patterson 已提交
155
    deprecate def formats=(_); end
A
Aaron Patterson 已提交
156
    deprecate def formats; Array(format); end
A
Aaron Patterson 已提交
157 158
    deprecate def variants=(_); end
    deprecate def variants; [variant]; end
A
Aaron Patterson 已提交
159

160
    # Returns whether the underlying handler supports streaming. If so,
161
    # a streaming buffer *may* be passed when it starts rendering.
162 163 164 165
    def supports_streaming?
      handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
    end

166 167 168 169 170 171
    # Render a template. If the template was not compiled yet, it is done
    # exactly before rendering.
    #
    # This method is instrumented as "!render_template.action_view". 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.
172
    def render(view, locals, buffer = ActionView::OutputBuffer.new, &block)
173
      instrument_render_template do
174
        compile!(view)
175
        view._run(method_name, self, locals, buffer, &block)
176
      end
177
    rescue => e
178
      handle_render_error(view, e)
179
    end
180

181
    def type
A
Aaron Patterson 已提交
182
      @type ||= Types[format]
183 184
    end

185 186 187 188 189 190 191 192 193
    # Receives a view object and return a template similar to self by using @virtual_path.
    #
    # This method is useful if you have a template object but it does not contain its source
    # anymore since it was already compiled. In such cases, all you need to do is to call
    # refresh passing in the view object.
    #
    # Notice this method raises an error if the template to be refreshed does not have a
    # virtual path set (true just for inline templates).
    def refresh(view)
194
      raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
195 196 197 198 199
      lookup  = view.lookup_context
      pieces  = @virtual_path.split("/")
      name    = pieces.pop
      partial = !!name.sub!(/^_/, "")
      lookup.disable_cache do
200
        lookup.find_template(name, [ pieces.join("/") ], partial, @locals)
201 202 203
      end
    end

204 205 206 207
    def short_identifier
      @short_identifier ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
    end

208
    def inspect
209
      "#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
210
    end
211

J
José Valim 已提交
212
    # This method is responsible for properly setting the encoding of the
J
José Valim 已提交
213 214 215 216 217 218 219 220 221 222
    # 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 <tt>Encoding.default_external</tt>.
    #
    # 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.
    def encode!
223 224 225
      source = self.source

      return source unless source.encoding == Encoding::BINARY
J
José Valim 已提交
226 227 228 229

      # Look for # encoding: *. If we find one, we'll encode the
      # String in that encoding, otherwise, we'll use the
      # default external encoding.
230
      if source.sub!(/\A#{ENCODING_FLAG}/, "")
J
José Valim 已提交
231 232 233 234
        encoding = magic_encoding = $1
      else
        encoding = Encoding.default_external
      end
J
José Valim 已提交
235

J
José Valim 已提交
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
      # Tag the source with the default external encoding
      # or the encoding specified in the file
      source.force_encoding(encoding)

      # If the user didn't specify an encoding, and the handler
      # handles encodings, we simply pass the String as is to
      # the handler (with the default_external tag)
      if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
        source
      # Otherwise, if the String is valid in the encoding,
      # encode immediately to default_internal. This means
      # that if a handler doesn't handle encodings, it will
      # always get Strings in the default_internal
      elsif source.valid_encoding?
        source.encode!
      # Otherwise, since the String is invalid in the encoding
      # specified, raise an exception
      else
        raise WrongEncodingError.new(source, encoding)
J
José Valim 已提交
255 256 257
      end
    end

B
bogdanvlviv 已提交
258

259 260 261
    # Exceptions are marshalled when using the parallel test runner with DRb, so we need
    # to ensure that references to the template object can be marshalled as well. This means forgoing
    # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
262
    def marshal_dump # :nodoc:
J
John Hawthorn 已提交
263
      [ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant ]
264 265
    end

266
    def marshal_load(array) # :nodoc:
J
John Hawthorn 已提交
267
      @source, @identifier, @handler, @compiled, @locals, @virtual_path, @format, @variant = *array
268 269 270
      @compile_mutex = Mutex.new
    end

271
    private
272

273 274
      # Compile a template. This method ensures a template is compiled
      # just once and removes the source after it is compiled.
275
      def compile!(view)
276
        return if @compiled
277

278 279 280 281 282 283 284 285 286
        # Templates can be used concurrently in threaded environments
        # so compilation and any instance variable modification must
        # be synchronized
        @compile_mutex.synchronize do
          # Any thread holding this lock will be compiling the template needed
          # by the threads waiting. So re-check the @compiled flag to avoid
          # re-compilation
          return if @compiled

287
          mod = view.compiled_method_container
288

289
          instrument("!compile_template") do
A
Aaron Patterson 已提交
290
            compile(mod)
291
          end
292 293 294 295 296

          # Just discard the source if we have a virtual path. This
          # means we can get the template back.
          @source = nil if @virtual_path
          @compiled = true
297 298
        end
      end
299

300 301 302 303 304 305 306 307 308
      class LegacyTemplate < DelegateClass(Template) # :nodoc:
        attr_reader :source

        def initialize(template, source)
          super(template)
          @source = source
        end
      end

309
      # Among other things, this method is responsible for properly setting
J
José Valim 已提交
310
      # the encoding of the compiled template.
311
      #
312 313 314 315 316 317
      # If the template engine handles encodings, we send the encoded
      # String to the engine without further processing. This allows
      # the template engine to support additional mechanisms for
      # specifying the encoding. For instance, ERB supports <%# encoding: %>
      #
      # Otherwise, after we figure out the correct encoding, we then
318 319
      # encode the source into <tt>Encoding.default_internal</tt>.
      # In general, this means that templates will be UTF-8 inside of Rails,
320
      # regardless of the original source encoding.
321
      def compile(mod)
322
        source = encode!
323
        code = @handler.call(self, source)
324

D
diatmpravin 已提交
325
        # Make sure that the resulting String to be eval'd is in the
326
        # encoding of the code
327
        source = +<<-end_src
328
          def #{method_name}(local_assigns, output_buffer)
329
            @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
330 331 332
          end
        end_src

333 334
        # Make sure the source is in the encoding of the returned code
        source.force_encoding(code.encoding)
335

336 337 338
        # 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!
339

340 341 342 343
        # Now, validate that the source we got back from the template
        # handler is valid in the default_internal. This is for handlers
        # that handle encoding but screw up
        unless source.valid_encoding?
344
          raise WrongEncodingError.new(source, Encoding.default_internal)
345
        end
346

A
Aaron Patterson 已提交
347
        mod.module_eval(source, identifier, 0)
348
      end
349

350
      def handle_render_error(view, e)
351 352 353 354
        if e.is_a?(Template::Error)
          e.sub_template_of(self)
          raise e
        else
J
José Valim 已提交
355 356 357 358 359
          template = self
          unless template.source
            template = refresh(view)
            template.encode!
          end
360
          raise Template::Error.new(template)
361
        end
362 363
      end

364
      def locals_code
365 366
        # Only locals with valid variable names get set directly. Others will
        # still be available in local_assigns.
367
        locals = @locals - Module::RUBY_RESERVED_KEYWORDS
368
        locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
369

Y
yuuji.yaginuma 已提交
370
        # Assign for the same variable is to suppress unused variable warning
371
        locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" }
372 373
      end

374
      def method_name
A
Akira Matsuda 已提交
375
        @method_name ||= begin
376
          m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
377
          m.tr!("-", "_")
A
Akira Matsuda 已提交
378 379
          m
        end
380 381
      end

382
      def identifier_method_name
383
        short_identifier.tr("^a-z_", "_")
384
      end
385

386
      def instrument(action, &block) # :doc:
387
        ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
388 389 390
      end

      def instrument_render_template(&block)
391
        ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block)
392 393 394 395
      end

      def instrument_payload
        { virtual_path: @virtual_path, identifier: @identifier }
396
      end
397
  end
398
end