actionview 5.2.4.1 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of actionview might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +193 -79
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -2
- data/lib/action_view.rb +3 -2
- data/lib/action_view/base.rb +107 -10
- data/lib/action_view/buffers.rb +15 -0
- data/lib/action_view/cache_expiry.rb +54 -0
- data/lib/action_view/context.rb +5 -9
- data/lib/action_view/digestor.rb +12 -20
- data/lib/action_view/gem_version.rb +4 -4
- data/lib/action_view/helpers.rb +0 -2
- data/lib/action_view/helpers/asset_tag_helper.rb +7 -30
- data/lib/action_view/helpers/asset_url_helper.rb +4 -3
- data/lib/action_view/helpers/cache_helper.rb +18 -10
- data/lib/action_view/helpers/capture_helper.rb +4 -0
- data/lib/action_view/helpers/csp_helper.rb +4 -2
- data/lib/action_view/helpers/csrf_helper.rb +1 -1
- data/lib/action_view/helpers/date_helper.rb +69 -25
- data/lib/action_view/helpers/form_helper.rb +238 -6
- data/lib/action_view/helpers/form_options_helper.rb +27 -18
- data/lib/action_view/helpers/form_tag_helper.rb +12 -9
- data/lib/action_view/helpers/javascript_helper.rb +9 -8
- data/lib/action_view/helpers/number_helper.rb +5 -0
- data/lib/action_view/helpers/output_safety_helper.rb +1 -1
- data/lib/action_view/helpers/rendering_helper.rb +6 -4
- data/lib/action_view/helpers/sanitize_helper.rb +12 -18
- data/lib/action_view/helpers/tag_helper.rb +7 -6
- data/lib/action_view/helpers/tags/base.rb +9 -5
- data/lib/action_view/helpers/tags/color_field.rb +1 -1
- data/lib/action_view/helpers/tags/translator.rb +1 -6
- data/lib/action_view/helpers/text_helper.rb +3 -3
- data/lib/action_view/helpers/translation_helper.rb +16 -12
- data/lib/action_view/helpers/url_helper.rb +14 -14
- data/lib/action_view/layouts.rb +5 -5
- data/lib/action_view/log_subscriber.rb +6 -6
- data/lib/action_view/lookup_context.rb +73 -31
- data/lib/action_view/path_set.rb +5 -10
- data/lib/action_view/railtie.rb +24 -1
- data/lib/action_view/record_identifier.rb +2 -2
- data/lib/action_view/renderer/abstract_renderer.rb +56 -3
- data/lib/action_view/renderer/partial_renderer.rb +66 -55
- data/lib/action_view/renderer/partial_renderer/collection_caching.rb +62 -16
- data/lib/action_view/renderer/renderer.rb +16 -4
- data/lib/action_view/renderer/streaming_template_renderer.rb +5 -5
- data/lib/action_view/renderer/template_renderer.rb +24 -18
- data/lib/action_view/rendering.rb +51 -31
- data/lib/action_view/routing_url_for.rb +12 -11
- data/lib/action_view/template.rb +102 -70
- data/lib/action_view/template/error.rb +21 -1
- data/lib/action_view/template/handlers.rb +27 -1
- data/lib/action_view/template/handlers/builder.rb +2 -2
- data/lib/action_view/template/handlers/erb.rb +17 -7
- data/lib/action_view/template/handlers/erb/erubi.rb +7 -3
- data/lib/action_view/template/handlers/html.rb +1 -1
- data/lib/action_view/template/handlers/raw.rb +2 -2
- data/lib/action_view/template/html.rb +14 -5
- data/lib/action_view/template/inline.rb +22 -0
- data/lib/action_view/template/raw_file.rb +28 -0
- data/lib/action_view/template/resolver.rb +136 -133
- data/lib/action_view/template/sources.rb +13 -0
- data/lib/action_view/template/sources/file.rb +17 -0
- data/lib/action_view/template/text.rb +5 -3
- data/lib/action_view/test_case.rb +1 -1
- data/lib/action_view/testing/resolvers.rb +33 -20
- data/lib/action_view/unbound_template.rb +32 -0
- data/lib/action_view/view_paths.rb +25 -1
- data/lib/assets/compiled/rails-ujs.js +29 -3
- metadata +25 -17
- data/lib/action_view/helpers/record_tag_helper.rb +0 -23
| @@ -109,7 +109,7 @@ module ActionView | |
| 109 109 | 
             
                      end
         | 
| 110 110 | 
             
                  end
         | 
| 111 111 |  | 
| 112 | 
            -
                  def  | 
| 112 | 
            +
                  def annotated_source_code
         | 
| 113 113 | 
             
                    source_extract(4)
         | 
| 114 114 | 
             
                  end
         | 
| 115 115 |  | 
| @@ -138,4 +138,24 @@ module ActionView | |
| 138 138 | 
             
              end
         | 
| 139 139 |  | 
| 140 140 | 
             
              TemplateError = Template::Error
         | 
| 141 | 
            +
             | 
| 142 | 
            +
              class SyntaxErrorInTemplate < TemplateError #:nodoc
         | 
| 143 | 
            +
                def initialize(template, offending_code_string)
         | 
| 144 | 
            +
                  @offending_code_string = offending_code_string
         | 
| 145 | 
            +
                  super(template)
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                def message
         | 
| 149 | 
            +
                  <<~MESSAGE
         | 
| 150 | 
            +
                    Encountered a syntax error while rendering template: check #{@offending_code_string}
         | 
| 151 | 
            +
                  MESSAGE
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                def annotated_source_code
         | 
| 155 | 
            +
                  @offending_code_string.split("\n").map.with_index(1) { |line, index|
         | 
| 156 | 
            +
                    indentation = " " * 4
         | 
| 157 | 
            +
                    "#{index}:#{indentation}#{line}"
         | 
| 158 | 
            +
                  }
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
              end
         | 
| 141 161 | 
             
            end
         | 
| @@ -1,5 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "active_support/deprecation"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            module ActionView #:nodoc:
         | 
| 4 6 | 
             
              # = Action View Template Handlers
         | 
| 5 7 | 
             
              class Template #:nodoc:
         | 
| @@ -14,7 +16,7 @@ module ActionView #:nodoc: | |
| 14 16 | 
             
                    base.register_template_handler :erb, ERB.new
         | 
| 15 17 | 
             
                    base.register_template_handler :html, Html.new
         | 
| 16 18 | 
             
                    base.register_template_handler :builder, Builder.new
         | 
| 17 | 
            -
                    base.register_template_handler :ruby,  | 
| 19 | 
            +
                    base.register_template_handler :ruby, lambda { |_, source| source }
         | 
| 18 20 | 
             
                  end
         | 
| 19 21 |  | 
| 20 22 | 
             
                  @@template_handlers = {}
         | 
| @@ -24,11 +26,35 @@ module ActionView #:nodoc: | |
| 24 26 | 
             
                    @@template_extensions ||= @@template_handlers.keys
         | 
| 25 27 | 
             
                  end
         | 
| 26 28 |  | 
| 29 | 
            +
                  class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
         | 
| 30 | 
            +
                    def call(view, source)
         | 
| 31 | 
            +
                      __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 27 35 | 
             
                  # Register an object that knows how to handle template files with the given
         | 
| 28 36 | 
             
                  # extensions. This can be used to implement new template types.
         | 
| 29 37 | 
             
                  # The handler must respond to +:call+, which will be passed the template
         | 
| 30 38 | 
             
                  # and should return the rendered template as a String.
         | 
| 31 39 | 
             
                  def register_template_handler(*extensions, handler)
         | 
| 40 | 
            +
                    params = if handler.is_a?(Proc)
         | 
| 41 | 
            +
                      handler.parameters
         | 
| 42 | 
            +
                    else
         | 
| 43 | 
            +
                      handler.method(:call).parameters
         | 
| 44 | 
            +
                    end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
         | 
| 47 | 
            +
                      ActiveSupport::Deprecation.warn <<~eowarn
         | 
| 48 | 
            +
                      Single arity template handlers are deprecated. Template handlers must
         | 
| 49 | 
            +
                      now accept two parameters, the view object and the source for the view object.
         | 
| 50 | 
            +
                      Change:
         | 
| 51 | 
            +
                        >> #{handler}.call(#{params.map(&:last).join(", ")})
         | 
| 52 | 
            +
                      To:
         | 
| 53 | 
            +
                        >> #{handler}.call(#{params.map(&:last).join(", ")}, source)
         | 
| 54 | 
            +
                      eowarn
         | 
| 55 | 
            +
                      handler = LegacyHandlerWrapper.new(handler)
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
             | 
| 32 58 | 
             
                    raise(ArgumentError, "Extension is required") if extensions.empty?
         | 
| 33 59 | 
             
                    extensions.each do |extension|
         | 
| 34 60 | 
             
                      @@template_handlers[extension.to_sym] = handler
         | 
| @@ -5,11 +5,11 @@ module ActionView | |
| 5 5 | 
             
                class Builder
         | 
| 6 6 | 
             
                  class_attribute :default_format, default: :xml
         | 
| 7 7 |  | 
| 8 | 
            -
                  def call(template)
         | 
| 8 | 
            +
                  def call(template, source)
         | 
| 9 9 | 
             
                    require_engine
         | 
| 10 10 | 
             
                    "xml = ::Builder::XmlMarkup.new(:indent => 2);" \
         | 
| 11 11 | 
             
                      "self.output_buffer = xml.target!;" +
         | 
| 12 | 
            -
                       | 
| 12 | 
            +
                      source +
         | 
| 13 13 | 
             
                      ";xml.target!;"
         | 
| 14 14 | 
             
                  end
         | 
| 15 15 |  | 
| @@ -14,12 +14,22 @@ module ActionView | |
| 14 14 | 
             
                    class_attribute :erb_implementation, default: Erubi
         | 
| 15 15 |  | 
| 16 16 | 
             
                    # Do not escape templates of these mime types.
         | 
| 17 | 
            -
                    class_attribute : | 
| 17 | 
            +
                    class_attribute :escape_ignore_list, default: ["text/plain"]
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    [self, singleton_class].each do |base|
         | 
| 20 | 
            +
                      base.alias_method :escape_whitelist, :escape_ignore_list
         | 
| 21 | 
            +
                      base.alias_method :escape_whitelist=, :escape_ignore_list=
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      base.deprecate(
         | 
| 24 | 
            +
                        escape_whitelist: "use #escape_ignore_list instead",
         | 
| 25 | 
            +
                        :escape_whitelist= => "use #escape_ignore_list= instead"
         | 
| 26 | 
            +
                      )
         | 
| 27 | 
            +
                    end
         | 
| 18 28 |  | 
| 19 29 | 
             
                    ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
         | 
| 20 30 |  | 
| 21 | 
            -
                    def self.call(template)
         | 
| 22 | 
            -
                      new.call(template)
         | 
| 31 | 
            +
                    def self.call(template, source)
         | 
| 32 | 
            +
                      new.call(template, source)
         | 
| 23 33 | 
             
                    end
         | 
| 24 34 |  | 
| 25 35 | 
             
                    def supports_streaming?
         | 
| @@ -30,24 +40,24 @@ module ActionView | |
| 30 40 | 
             
                      true
         | 
| 31 41 | 
             
                    end
         | 
| 32 42 |  | 
| 33 | 
            -
                    def call(template)
         | 
| 43 | 
            +
                    def call(template, source)
         | 
| 34 44 | 
             
                      # First, convert to BINARY, so in case the encoding is
         | 
| 35 45 | 
             
                      # wrong, we can still find an encoding tag
         | 
| 36 46 | 
             
                      # (<%# encoding %>) inside the String using a regular
         | 
| 37 47 | 
             
                      # expression
         | 
| 38 | 
            -
                      template_source =  | 
| 48 | 
            +
                      template_source = source.dup.force_encoding(Encoding::ASCII_8BIT)
         | 
| 39 49 |  | 
| 40 50 | 
             
                      erb = template_source.gsub(ENCODING_TAG, "")
         | 
| 41 51 | 
             
                      encoding = $2
         | 
| 42 52 |  | 
| 43 | 
            -
                      erb.force_encoding valid_encoding( | 
| 53 | 
            +
                      erb.force_encoding valid_encoding(source.dup, encoding)
         | 
| 44 54 |  | 
| 45 55 | 
             
                      # Always make sure we return a String in the default_internal
         | 
| 46 56 | 
             
                      erb.encode!
         | 
| 47 57 |  | 
| 48 58 | 
             
                      self.class.erb_implementation.new(
         | 
| 49 59 | 
             
                        erb,
         | 
| 50 | 
            -
                        escape: (self.class. | 
| 60 | 
            +
                        escape: (self.class.escape_ignore_list.include? template.type),
         | 
| 51 61 | 
             
                        trim: (self.class.erb_trim_mode == "-")
         | 
| 52 62 | 
             
                      ).src
         | 
| 53 63 | 
             
                    end
         | 
| @@ -13,7 +13,7 @@ module ActionView | |
| 13 13 |  | 
| 14 14 | 
             
                        # Dup properties so that we don't modify argument
         | 
| 15 15 | 
             
                        properties = Hash[properties]
         | 
| 16 | 
            -
                        properties[:preamble]   = " | 
| 16 | 
            +
                        properties[:preamble]   = ""
         | 
| 17 17 | 
             
                        properties[:postamble]  = "@output_buffer.to_s"
         | 
| 18 18 | 
             
                        properties[:bufvar]     = "@output_buffer"
         | 
| 19 19 | 
             
                        properties[:escapefunc] = ""
         | 
| @@ -22,8 +22,12 @@ module ActionView | |
| 22 22 | 
             
                      end
         | 
| 23 23 |  | 
| 24 24 | 
             
                      def evaluate(action_view_erb_handler_context)
         | 
| 25 | 
            -
                         | 
| 26 | 
            -
                         | 
| 25 | 
            +
                        src = @src
         | 
| 26 | 
            +
                        view = Class.new(ActionView::Base) {
         | 
| 27 | 
            +
                          include action_view_erb_handler_context._routes.url_helpers
         | 
| 28 | 
            +
                          class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
         | 
| 29 | 
            +
                        }.empty
         | 
| 30 | 
            +
                        view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
         | 
| 27 31 | 
             
                      end
         | 
| 28 32 |  | 
| 29 33 | 
             
                    private
         | 
| @@ -1,15 +1,21 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "active_support/deprecation"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            module ActionView #:nodoc:
         | 
| 4 6 | 
             
              # = Action View HTML Template
         | 
| 5 7 | 
             
              class Template #:nodoc:
         | 
| 6 8 | 
             
                class HTML #:nodoc:
         | 
| 7 | 
            -
                   | 
| 9 | 
            +
                  attr_reader :type
         | 
| 8 10 |  | 
| 9 11 | 
             
                  def initialize(string, type = nil)
         | 
| 12 | 
            +
                    unless type
         | 
| 13 | 
            +
                      ActiveSupport::Deprecation.warn "ActionView::Template::HTML#initialize requires a type parameter"
         | 
| 14 | 
            +
                      type = :html
         | 
| 15 | 
            +
                    end
         | 
| 16 | 
            +
             | 
| 10 17 | 
             
                    @string = string.to_s
         | 
| 11 | 
            -
                    @type   =  | 
| 12 | 
            -
                    @type ||= Types[:html]
         | 
| 18 | 
            +
                    @type   = type
         | 
| 13 19 | 
             
                  end
         | 
| 14 20 |  | 
| 15 21 | 
             
                  def identifier
         | 
| @@ -26,9 +32,12 @@ module ActionView #:nodoc: | |
| 26 32 | 
             
                    to_str
         | 
| 27 33 | 
             
                  end
         | 
| 28 34 |  | 
| 29 | 
            -
                  def  | 
| 30 | 
            -
                     | 
| 35 | 
            +
                  def format
         | 
| 36 | 
            +
                    @type
         | 
| 31 37 | 
             
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def formats; Array(format); end
         | 
| 40 | 
            +
                  deprecate :formats
         | 
| 32 41 | 
             
                end
         | 
| 33 42 | 
             
              end
         | 
| 34 43 | 
             
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ActionView #:nodoc:
         | 
| 4 | 
            +
              class Template #:nodoc:
         | 
| 5 | 
            +
                class Inline < Template #:nodoc:
         | 
| 6 | 
            +
                  # This finalizer is needed (and exactly with a proc inside another proc)
         | 
| 7 | 
            +
                  # otherwise templates leak in development.
         | 
| 8 | 
            +
                  Finalizer = proc do |method_name, mod| # :nodoc:
         | 
| 9 | 
            +
                    proc do
         | 
| 10 | 
            +
                      mod.module_eval do
         | 
| 11 | 
            +
                        remove_possible_method method_name
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def compile(mod)
         | 
| 17 | 
            +
                    super
         | 
| 18 | 
            +
                    ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ActionView #:nodoc:
         | 
| 4 | 
            +
              # = Action View RawFile Template
         | 
| 5 | 
            +
              class Template #:nodoc:
         | 
| 6 | 
            +
                class RawFile #:nodoc:
         | 
| 7 | 
            +
                  attr_accessor :type, :format
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def initialize(filename)
         | 
| 10 | 
            +
                    @filename = filename.to_s
         | 
| 11 | 
            +
                    extname = ::File.extname(filename).delete(".")
         | 
| 12 | 
            +
                    @type = Template::Types[extname] || Template::Types[:text]
         | 
| 13 | 
            +
                    @format = @type.symbol
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def identifier
         | 
| 17 | 
            +
                    @filename
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def render(*args)
         | 
| 21 | 
            +
                    ::File.read(@filename)
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def formats; Array(format); end
         | 
| 25 | 
            +
                  deprecate :formats
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| @@ -16,7 +16,7 @@ module ActionView | |
| 16 16 | 
             
                  alias_method :partial?, :partial
         | 
| 17 17 |  | 
| 18 18 | 
             
                  def self.build(name, prefix, partial)
         | 
| 19 | 
            -
                    virtual = "" | 
| 19 | 
            +
                    virtual = +""
         | 
| 20 20 | 
             
                    virtual << "#{prefix}/" unless prefix.empty?
         | 
| 21 21 | 
             
                    virtual << (partial ? "_#{name}" : name)
         | 
| 22 22 | 
             
                    new name, prefix, partial, virtual
         | 
| @@ -63,26 +63,11 @@ module ActionView | |
| 63 63 |  | 
| 64 64 | 
             
                  # Cache the templates returned by the block
         | 
| 65 65 | 
             
                  def cache(key, name, prefix, partial, locals)
         | 
| 66 | 
            -
                     | 
| 67 | 
            -
                      @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
         | 
| 68 | 
            -
                    else
         | 
| 69 | 
            -
                      fresh_templates  = yield
         | 
| 70 | 
            -
                      cached_templates = @data[key][name][prefix][partial][locals]
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                      if templates_have_changed?(cached_templates, fresh_templates)
         | 
| 73 | 
            -
                        @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
         | 
| 74 | 
            -
                      else
         | 
| 75 | 
            -
                        cached_templates || NO_TEMPLATES
         | 
| 76 | 
            -
                      end
         | 
| 77 | 
            -
                    end
         | 
| 66 | 
            +
                    @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
         | 
| 78 67 | 
             
                  end
         | 
| 79 68 |  | 
| 80 69 | 
             
                  def cache_query(query) # :nodoc:
         | 
| 81 | 
            -
                     | 
| 82 | 
            -
                      @query_cache[query] ||= canonical_no_templates(yield)
         | 
| 83 | 
            -
                    else
         | 
| 84 | 
            -
                      yield
         | 
| 85 | 
            -
                    end
         | 
| 70 | 
            +
                    @query_cache[query] ||= canonical_no_templates(yield)
         | 
| 86 71 | 
             
                  end
         | 
| 87 72 |  | 
| 88 73 | 
             
                  def clear
         | 
| @@ -112,19 +97,6 @@ module ActionView | |
| 112 97 | 
             
                    def canonical_no_templates(templates)
         | 
| 113 98 | 
             
                      templates.empty? ? NO_TEMPLATES : templates
         | 
| 114 99 | 
             
                    end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                    def templates_have_changed?(cached_templates, fresh_templates)
         | 
| 117 | 
            -
                      # if either the old or new template list is empty, we don't need to (and can't)
         | 
| 118 | 
            -
                      # compare modification times, and instead just check whether the lists are different
         | 
| 119 | 
            -
                      if cached_templates.blank? || fresh_templates.blank?
         | 
| 120 | 
            -
                        return fresh_templates.blank? != cached_templates.blank?
         | 
| 121 | 
            -
                      end
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                      cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
         | 
| 124 | 
            -
             | 
| 125 | 
            -
                      # if a template has changed, it will be now be newer than all the cached templates
         | 
| 126 | 
            -
                      fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
         | 
| 127 | 
            -
                    end
         | 
| 128 100 | 
             
                end
         | 
| 129 101 |  | 
| 130 102 | 
             
                cattr_accessor :caching, default: true
         | 
| @@ -143,35 +115,33 @@ module ActionView | |
| 143 115 |  | 
| 144 116 | 
             
                # Normalizes the arguments and passes it on to find_templates.
         | 
| 145 117 | 
             
                def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
         | 
| 146 | 
            -
                   | 
| 147 | 
            -
                    find_templates(name, prefix, partial, details)
         | 
| 148 | 
            -
                  end
         | 
| 149 | 
            -
                end
         | 
| 118 | 
            +
                  locals = locals.map(&:to_s).sort!.freeze
         | 
| 150 119 |  | 
| 151 | 
            -
                def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
         | 
| 152 120 | 
             
                  cached(key, [name, prefix, partial], details, locals) do
         | 
| 153 | 
            -
                     | 
| 121 | 
            +
                    _find_all(name, prefix, partial, details, key, locals)
         | 
| 154 122 | 
             
                  end
         | 
| 155 123 | 
             
                end
         | 
| 156 124 |  | 
| 125 | 
            +
                alias :find_all_anywhere :find_all
         | 
| 126 | 
            +
                deprecate :find_all_anywhere
         | 
| 127 | 
            +
             | 
| 157 128 | 
             
                def find_all_with_query(query) # :nodoc:
         | 
| 158 129 | 
             
                  @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
         | 
| 159 130 | 
             
                end
         | 
| 160 131 |  | 
| 161 132 | 
             
              private
         | 
| 162 133 |  | 
| 134 | 
            +
                def _find_all(name, prefix, partial, details, key, locals)
         | 
| 135 | 
            +
                  find_templates(name, prefix, partial, details, locals)
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 163 138 | 
             
                delegate :caching?, to: :class
         | 
| 164 139 |  | 
| 165 140 | 
             
                # This is what child classes implement. No defaults are needed
         | 
| 166 141 | 
             
                # because Resolver guarantees that the arguments are present and
         | 
| 167 142 | 
             
                # normalized.
         | 
| 168 | 
            -
                def find_templates(name, prefix, partial, details,  | 
| 169 | 
            -
                  raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details,  | 
| 170 | 
            -
                end
         | 
| 171 | 
            -
             | 
| 172 | 
            -
                # Helpers that builds a path. Useful for building virtual paths.
         | 
| 173 | 
            -
                def build_path(name, prefix, partial)
         | 
| 174 | 
            -
                  Path.build(name, prefix, partial)
         | 
| 143 | 
            +
                def find_templates(name, prefix, partial, details, locals = [])
         | 
| 144 | 
            +
                  raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
         | 
| 175 145 | 
             
                end
         | 
| 176 146 |  | 
| 177 147 | 
             
                # Handles templates caching. If a key is given and caching is on
         | 
| @@ -180,25 +150,13 @@ module ActionView | |
| 180 150 | 
             
                # resolver is fresher before returning it.
         | 
| 181 151 | 
             
                def cached(key, path_info, details, locals)
         | 
| 182 152 | 
             
                  name, prefix, partial = path_info
         | 
| 183 | 
            -
                  locals = locals.map(&:to_s).sort!
         | 
| 184 153 |  | 
| 185 154 | 
             
                  if key
         | 
| 186 155 | 
             
                    @cache.cache(key, name, prefix, partial, locals) do
         | 
| 187 | 
            -
                       | 
| 156 | 
            +
                      yield
         | 
| 188 157 | 
             
                    end
         | 
| 189 158 | 
             
                  else
         | 
| 190 | 
            -
                     | 
| 191 | 
            -
                  end
         | 
| 192 | 
            -
                end
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                # Ensures all the resolver information is set in the template.
         | 
| 195 | 
            -
                def decorate(templates, path_info, details, locals)
         | 
| 196 | 
            -
                  cached = nil
         | 
| 197 | 
            -
                  templates.each do |t|
         | 
| 198 | 
            -
                    t.locals         = locals
         | 
| 199 | 
            -
                    t.formats        = details[:formats]  || [:html] if t.formats.empty?
         | 
| 200 | 
            -
                    t.variants       = details[:variants] || []      if t.variants.empty?
         | 
| 201 | 
            -
                    t.virtual_path ||= (cached ||= build_path(*path_info))
         | 
| 159 | 
            +
                    yield
         | 
| 202 160 | 
             
                  end
         | 
| 203 161 | 
             
                end
         | 
| 204 162 | 
             
              end
         | 
| @@ -209,40 +167,69 @@ module ActionView | |
| 209 167 | 
             
                DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
         | 
| 210 168 |  | 
| 211 169 | 
             
                def initialize(pattern = nil)
         | 
| 212 | 
            -
                   | 
| 170 | 
            +
                  if pattern
         | 
| 171 | 
            +
                    ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
         | 
| 172 | 
            +
                    @pattern = pattern
         | 
| 173 | 
            +
                  else
         | 
| 174 | 
            +
                    @pattern = DEFAULT_PATTERN
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
                  @unbound_templates = Concurrent::Map.new
         | 
| 177 | 
            +
                  super()
         | 
| 178 | 
            +
                end
         | 
| 179 | 
            +
             | 
| 180 | 
            +
                def clear_cache
         | 
| 181 | 
            +
                  @unbound_templates.clear
         | 
| 213 182 | 
             
                  super()
         | 
| 214 183 | 
             
                end
         | 
| 215 184 |  | 
| 216 185 | 
             
                private
         | 
| 217 186 |  | 
| 218 | 
            -
                  def  | 
| 187 | 
            +
                  def _find_all(name, prefix, partial, details, key, locals)
         | 
| 219 188 | 
             
                    path = Path.build(name, prefix, partial)
         | 
| 220 | 
            -
                    query(path, details, details[:formats],  | 
| 189 | 
            +
                    query(path, details, details[:formats], locals, cache: !!key)
         | 
| 221 190 | 
             
                  end
         | 
| 222 191 |  | 
| 223 | 
            -
                  def query(path, details, formats,  | 
| 224 | 
            -
                     | 
| 225 | 
            -
             | 
| 226 | 
            -
                    template_paths = find_template_paths(query)
         | 
| 227 | 
            -
                    template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
         | 
| 192 | 
            +
                  def query(path, details, formats, locals, cache:)
         | 
| 193 | 
            +
                    template_paths = find_template_paths_from_details(path, details)
         | 
| 194 | 
            +
                    template_paths = reject_files_external_to_app(template_paths)
         | 
| 228 195 |  | 
| 229 196 | 
             
                    template_paths.map do |template|
         | 
| 230 | 
            -
                       | 
| 231 | 
            -
             | 
| 232 | 
            -
             | 
| 233 | 
            -
             | 
| 234 | 
            -
             | 
| 235 | 
            -
                         | 
| 236 | 
            -
             | 
| 237 | 
            -
                         | 
| 238 | 
            -
             | 
| 197 | 
            +
                      unbound_template =
         | 
| 198 | 
            +
                        if cache
         | 
| 199 | 
            +
                          @unbound_templates.compute_if_absent([template, path.virtual]) do
         | 
| 200 | 
            +
                            build_unbound_template(template, path.virtual)
         | 
| 201 | 
            +
                          end
         | 
| 202 | 
            +
                        else
         | 
| 203 | 
            +
                          build_unbound_template(template, path.virtual)
         | 
| 204 | 
            +
                        end
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                      unbound_template.bind_locals(locals)
         | 
| 239 207 | 
             
                    end
         | 
| 240 208 | 
             
                  end
         | 
| 241 209 |  | 
| 210 | 
            +
                  def build_unbound_template(template, virtual_path)
         | 
| 211 | 
            +
                    handler, format, variant = extract_handler_and_format_and_variant(template)
         | 
| 212 | 
            +
                    source = Template::Sources::File.new(template)
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                    UnboundTemplate.new(
         | 
| 215 | 
            +
                      source,
         | 
| 216 | 
            +
                      template,
         | 
| 217 | 
            +
                      handler,
         | 
| 218 | 
            +
                      virtual_path: virtual_path,
         | 
| 219 | 
            +
                      format: format,
         | 
| 220 | 
            +
                      variant: variant,
         | 
| 221 | 
            +
                    )
         | 
| 222 | 
            +
                  end
         | 
| 223 | 
            +
             | 
| 242 224 | 
             
                  def reject_files_external_to_app(files)
         | 
| 243 225 | 
             
                    files.reject { |filename| !inside_path?(@path, filename) }
         | 
| 244 226 | 
             
                  end
         | 
| 245 227 |  | 
| 228 | 
            +
                  def find_template_paths_from_details(path, details)
         | 
| 229 | 
            +
                    query = build_query(path, details)
         | 
| 230 | 
            +
                    find_template_paths(query)
         | 
| 231 | 
            +
                  end
         | 
| 232 | 
            +
             | 
| 246 233 | 
             
                  def find_template_paths(query)
         | 
| 247 234 | 
             
                    Dir[query].uniq.reject do |filename|
         | 
| 248 235 | 
             
                      File.directory?(filename) ||
         | 
| @@ -279,70 +266,39 @@ module ActionView | |
| 279 266 | 
             
                  end
         | 
| 280 267 |  | 
| 281 268 | 
             
                  def escape_entry(entry)
         | 
| 282 | 
            -
                    entry.gsub(/[*?{}\[\]]/, '\\\\\\&' | 
| 283 | 
            -
                  end
         | 
| 284 | 
            -
             | 
| 285 | 
            -
                  # Returns the file mtime from the filesystem.
         | 
| 286 | 
            -
                  def mtime(p)
         | 
| 287 | 
            -
                    File.mtime(p)
         | 
| 269 | 
            +
                    entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
         | 
| 288 270 | 
             
                  end
         | 
| 289 271 |  | 
| 290 272 | 
             
                  # Extract handler, formats and variant from path. If a format cannot be found neither
         | 
| 291 273 | 
             
                  # from the path, or the handler, we should return the array of formats given
         | 
| 292 274 | 
             
                  # to the resolver.
         | 
| 293 275 | 
             
                  def extract_handler_and_format_and_variant(path)
         | 
| 294 | 
            -
                    pieces = File.basename(path).split("." | 
| 276 | 
            +
                    pieces = File.basename(path).split(".")
         | 
| 295 277 | 
             
                    pieces.shift
         | 
| 296 278 |  | 
| 297 279 | 
             
                    extension = pieces.pop
         | 
| 298 280 |  | 
| 299 281 | 
             
                    handler = Template.handler_for_extension(extension)
         | 
| 300 282 | 
             
                    format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
         | 
| 301 | 
            -
                    format  | 
| 283 | 
            +
                    format = if format
         | 
| 284 | 
            +
                      Template::Types[format]&.ref
         | 
| 285 | 
            +
                    else
         | 
| 286 | 
            +
                      if handler.respond_to?(:default_format) # default_format can return nil
         | 
| 287 | 
            +
                        handler.default_format
         | 
| 288 | 
            +
                      else
         | 
| 289 | 
            +
                        nil
         | 
| 290 | 
            +
                      end
         | 
| 291 | 
            +
                    end
         | 
| 302 292 |  | 
| 293 | 
            +
                    # Template::Types[format] and handler.default_format can return nil
         | 
| 303 294 | 
             
                    [handler, format, variant]
         | 
| 304 295 | 
             
                  end
         | 
| 305 296 | 
             
              end
         | 
| 306 297 |  | 
| 307 | 
            -
              # A resolver that loads files from the filesystem. | 
| 308 | 
            -
              # resolving pattern. Such pattern can be a glob string supported by some variables.
         | 
| 309 | 
            -
              #
         | 
| 310 | 
            -
              # ==== Examples
         | 
| 311 | 
            -
              #
         | 
| 312 | 
            -
              # Default pattern, loads views the same way as previous versions of rails, eg. when you're
         | 
| 313 | 
            -
              # looking for <tt>users/new</tt> it will produce query glob: <tt>users/new{.{en},}{.{html,js},}{.{erb,haml},}</tt>
         | 
| 314 | 
            -
              #
         | 
| 315 | 
            -
              #   FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
         | 
| 316 | 
            -
              #
         | 
| 317 | 
            -
              # This one allows you to keep files with different formats in separate subdirectories,
         | 
| 318 | 
            -
              # eg. <tt>users/new.html</tt> will be loaded from <tt>users/html/new.erb</tt> or <tt>users/new.html.erb</tt>,
         | 
| 319 | 
            -
              # <tt>users/new.js</tt> from <tt>users/js/new.erb</tt> or <tt>users/new.js.erb</tt>, etc.
         | 
| 320 | 
            -
              #
         | 
| 321 | 
            -
              #   FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
         | 
| 322 | 
            -
              #
         | 
| 323 | 
            -
              # If you don't specify a pattern then the default will be used.
         | 
| 324 | 
            -
              #
         | 
| 325 | 
            -
              # In order to use any of the customized resolvers above in a Rails application, you just need
         | 
| 326 | 
            -
              # to configure ActionController::Base.view_paths in an initializer, for example:
         | 
| 327 | 
            -
              #
         | 
| 328 | 
            -
              #   ActionController::Base.view_paths = FileSystemResolver.new(
         | 
| 329 | 
            -
              #     Rails.root.join("app/views"),
         | 
| 330 | 
            -
              #     ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
         | 
| 331 | 
            -
              #   )
         | 
| 332 | 
            -
              #
         | 
| 333 | 
            -
              # ==== Pattern format and variables
         | 
| 334 | 
            -
              #
         | 
| 335 | 
            -
              # Pattern has to be a valid glob string, and it allows you to use the
         | 
| 336 | 
            -
              # following variables:
         | 
| 337 | 
            -
              #
         | 
| 338 | 
            -
              # * <tt>:prefix</tt> - usually the controller path
         | 
| 339 | 
            -
              # * <tt>:action</tt> - name of the action
         | 
| 340 | 
            -
              # * <tt>:locale</tt> - possible locale versions
         | 
| 341 | 
            -
              # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
         | 
| 342 | 
            -
              # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
         | 
| 343 | 
            -
              # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
         | 
| 344 | 
            -
              #
         | 
| 298 | 
            +
              # A resolver that loads files from the filesystem.
         | 
| 345 299 | 
             
              class FileSystemResolver < PathResolver
         | 
| 300 | 
            +
                attr_reader :path
         | 
| 301 | 
            +
             | 
| 346 302 | 
             
                def initialize(path, pattern = nil)
         | 
| 347 303 | 
             
                  raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
         | 
| 348 304 | 
             
                  super(pattern)
         | 
| @@ -362,30 +318,77 @@ module ActionView | |
| 362 318 |  | 
| 363 319 | 
             
              # An Optimized resolver for Rails' most common case.
         | 
| 364 320 | 
             
              class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
         | 
| 365 | 
            -
                def  | 
| 366 | 
            -
                   | 
| 321 | 
            +
                def initialize(path)
         | 
| 322 | 
            +
                  super(path)
         | 
| 323 | 
            +
                end
         | 
| 367 324 |  | 
| 368 | 
            -
             | 
| 369 | 
            -
             | 
| 370 | 
            -
             | 
| 371 | 
            -
                     | 
| 372 | 
            -
             | 
| 325 | 
            +
                private
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                  def find_template_paths_from_details(path, details)
         | 
| 328 | 
            +
                    # Instead of checking for every possible path, as our other globs would
         | 
| 329 | 
            +
                    # do, scan the directory for files with the right prefix.
         | 
| 330 | 
            +
                    query = "#{escape_entry(File.join(@path, path))}*"
         | 
| 331 | 
            +
             | 
| 332 | 
            +
                    regex = build_regex(path, details)
         | 
| 333 | 
            +
             | 
| 334 | 
            +
                    Dir[query].uniq.reject do |filename|
         | 
| 335 | 
            +
                      # This regex match does double duty of finding only files which match
         | 
| 336 | 
            +
                      # details (instead of just matching the prefix) and also filtering for
         | 
| 337 | 
            +
                      # case-insensitive file systems.
         | 
| 338 | 
            +
                      !regex.match?(filename) ||
         | 
| 339 | 
            +
                        File.directory?(filename)
         | 
| 340 | 
            +
                    end.sort_by do |filename|
         | 
| 341 | 
            +
                      # Because we scanned the directory, instead of checking for files
         | 
| 342 | 
            +
                      # one-by-one, they will be returned in an arbitrary order.
         | 
| 343 | 
            +
                      # We can use the matches found by the regex and sort by their index in
         | 
| 344 | 
            +
                      # details.
         | 
| 345 | 
            +
                      match = filename.match(regex)
         | 
| 346 | 
            +
                      EXTENSIONS.keys.reverse.map do |ext|
         | 
| 347 | 
            +
                        if ext == :variants && details[ext] == :any
         | 
| 348 | 
            +
                          match[ext].nil? ? 0 : 1
         | 
| 349 | 
            +
                        elsif match[ext].nil?
         | 
| 350 | 
            +
                          # No match should be last
         | 
| 351 | 
            +
                          details[ext].length
         | 
| 352 | 
            +
                        else
         | 
| 353 | 
            +
                          found = match[ext].to_sym
         | 
| 354 | 
            +
                          details[ext].index(found)
         | 
| 355 | 
            +
                        end
         | 
| 356 | 
            +
                      end
         | 
| 373 357 | 
             
                    end
         | 
| 374 | 
            -
                  end | 
| 358 | 
            +
                  end
         | 
| 375 359 |  | 
| 376 | 
            -
                   | 
| 377 | 
            -
             | 
| 360 | 
            +
                  def build_regex(path, details)
         | 
| 361 | 
            +
                    query = escape_entry(File.join(@path, path))
         | 
| 362 | 
            +
                    exts = EXTENSIONS.map do |ext, prefix|
         | 
| 363 | 
            +
                      match =
         | 
| 364 | 
            +
                        if ext == :variants && details[ext] == :any
         | 
| 365 | 
            +
                          ".*?"
         | 
| 366 | 
            +
                        else
         | 
| 367 | 
            +
                          details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|")
         | 
| 368 | 
            +
                        end
         | 
| 369 | 
            +
                      prefix = Regexp.escape(prefix)
         | 
| 370 | 
            +
                      "(#{prefix}(?<#{ext}>#{match}))?"
         | 
| 371 | 
            +
                    end.join
         | 
| 372 | 
            +
             | 
| 373 | 
            +
                    %r{\A#{query}#{exts}\z}
         | 
| 374 | 
            +
                  end
         | 
| 378 375 | 
             
              end
         | 
| 379 376 |  | 
| 380 377 | 
             
              # The same as FileSystemResolver but does not allow templates to store
         | 
| 381 378 | 
             
              # a virtual path since it is invalid for such resolvers.
         | 
| 382 379 | 
             
              class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
         | 
| 380 | 
            +
                private_class_method :new
         | 
| 381 | 
            +
             | 
| 383 382 | 
             
                def self.instances
         | 
| 384 383 | 
             
                  [new(""), new("/")]
         | 
| 385 384 | 
             
                end
         | 
| 386 385 |  | 
| 387 | 
            -
                def  | 
| 388 | 
            -
                  super | 
| 386 | 
            +
                def build_unbound_template(template, _)
         | 
| 387 | 
            +
                  super(template, nil)
         | 
| 388 | 
            +
                end
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                def reject_files_external_to_app(files)
         | 
| 391 | 
            +
                  files
         | 
| 389 392 | 
             
                end
         | 
| 390 393 | 
             
              end
         | 
| 391 394 | 
             
            end
         |