aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view/digestor.rb
blob: 666291e1c163339190a02daf81f1bbc81f9b39b7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
require 'thread_safe'
require 'action_view/dependency_tracker'
require 'monitor'

module ActionView
  class Digestor
    cattr_reader(:cache)
    @@cache          = ThreadSafe::Cache.new
    @@digest_monitor = Monitor.new

    class << self
      # Supported options:
      #
      # * <tt>name</tt>   - Template name
      # * <tt>format</tt>  - Template format
      # * <tt>variant</tt>  - Variant of +format+ (optional)
      # * <tt>finder</tt>  - An instance of ActionView::LookupContext
      # * <tt>dependencies</tt>  - An array of dependent views
      # * <tt>partial</tt>  - Specifies whether the template is a partial
      def digest(*args)
        options = _setup_options(*args)

        details_key = options[:finder].details_key.hash
        dependencies = Array.wrap(options[:dependencies])
        cache_key = ([options[:name], details_key, options[:format], options[:variant]].compact + dependencies).join('.')

        # this is a correctly done double-checked locking idiom
        # (ThreadSafe::Cache's lookups have volatile semantics)
        @@cache[cache_key] || @@digest_monitor.synchronize do
          @@cache.fetch(cache_key) do # re-check under lock
            compute_and_store_digest(cache_key, options)
          end
        end
      end

      def _setup_options(*args)
        unless args.first.is_a?(Hash)
          ActiveSupport::Deprecation.warn("Arguments to ActionView::Digestor should be provided as a hash. The support for regular arguments will be removed in Rails 5.0 or later")

          {
            name:   args.first,
            format: args.second,
            finder: args.third,
          }.merge(args.fourth || {})
        else
          options = args.first
          options.assert_valid_keys(:name, :format, :variant, :finder, :dependencies, :partial)

          options
        end
      end

      private

      def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
        klass = if options[:partial] || options[:name].include?("/_")
          # Prevent re-entry or else recursive templates will blow the stack.
          # There is no need to worry about other threads seeing the +false+ value,
          # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
          pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
          PartialDigestor
        else
          Digestor
        end

        digest = klass.new(options).digest
        # Store the actual digest if config.cache_template_loading is true
        @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
        digest
      ensure
        # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
        @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
      end
    end

    attr_reader :name, :format, :variant, :finder, :options

    def initialize(*args)
      @options = self.class._setup_options(*args)

      @name = @options.delete(:name)
      @format = @options.delete(:format)
      @variant = @options.delete(:variant)
      @finder = @options.delete(:finder)
    end

    def digest
      Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
        logger.try :info, "Cache digest for #{name}.#{format}: #{digest}"
      end
    rescue ActionView::MissingTemplate
      logger.try :error, "Couldn't find template for digesting: #{name}.#{format}"
      ''
    end

    def dependencies
      DependencyTracker.find_dependencies(name, template)
    rescue ActionView::MissingTemplate
      [] # File doesn't exist, so no dependencies
    end

    def nested_dependencies
      dependencies.collect do |dependency|
        dependencies = PartialDigestor.new(name: dependency, format: format, finder: finder).nested_dependencies
        dependencies.any? ? { dependency => dependencies } : dependency
      end
    end

    private

      def logger
        ActionView::Base.logger
      end

      def logical_name
        name.gsub(%r|/_|, "/")
      end

      def partial?
        false
      end

      def template
        @template ||= begin
          finder.formats  = [format]
          finder.variants = [variant]

          finder.disable_cache do
            finder.find(logical_name, [], partial?)
          end
        end
      end

      def source
        template.source
      end

      def dependency_digest
        template_digests = dependencies.collect do |template_name|
          Digestor.digest(name: template_name, format: format, finder: finder, partial: true)
        end

        (template_digests + injected_dependencies).join("-")
      end

      def injected_dependencies
        Array.wrap(options[:dependencies])
      end
  end

  class PartialDigestor < Digestor # :nodoc:
    def partial?
      true
    end
  end
end