cache.rb 3.7 KB
Newer Older
1 2
require 'active_support/core_ext/object/blank'

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
module ActionDispatch
  module Http
    module Cache
      module Request
        def if_modified_since
          if since = env['HTTP_IF_MODIFIED_SINCE']
            Time.rfc2822(since) rescue nil
          end
        end

        def if_none_match
          env['HTTP_IF_NONE_MATCH']
        end

        def not_modified?(modified_at)
          if_modified_since && modified_at && if_modified_since >= modified_at
        end

        def etag_matches?(etag)
          if_none_match && if_none_match == etag
        end

        # Check response freshness (Last-Modified and ETag) against request
        # If-Modified-Since and If-None-Match conditions. If both headers are
        # supplied, both must match, or the request is not considered fresh.
        def fresh?(response)
          last_modified = if_modified_since
          etag          = if_none_match

          return false unless last_modified || etag

          success = true
          success &&= not_modified?(response.last_modified) if last_modified
          success &&= etag_matches?(response.etag) if etag
          success
        end
      end

      module Response
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
        attr_reader :cache_control

        def initialize(*)
          status, header, body = super

          @cache_control = {}
          @etag = self["ETag"]

          if cache_control = self["Cache-Control"]
            cache_control.split(/,\s*/).each do |segment|
              first, last = segment.split("=")
              last ||= true
              @cache_control[first.to_sym] = last
            end
          end
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
        end

        def last_modified
          if last = headers['Last-Modified']
            Time.httpdate(last)
          end
        end

        def last_modified?
          headers.include?('Last-Modified')
        end

        def last_modified=(utc_time)
          headers['Last-Modified'] = utc_time.httpdate
        end

        def etag
          @etag
        end

        def etag?
          @etag
        end

        def etag=(etag)
          key = ActiveSupport::Cache.expand_cache_key(etag)
83
          @etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}")
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
        end

      private

        def handle_conditional_get!
          if etag? || last_modified? || !@cache_control.empty?
            set_conditional_cache_control!
          elsif nonempty_ok_response?
            self.etag = @body

            if request && request.etag_matches?(etag)
              self.status = 304
              self.body = []
            end

            set_conditional_cache_control!
          else
            headers["Cache-Control"] = "no-cache"
          end
        end

        def nonempty_ok_response?
          @status == 200 && string_body?
        end

        def string_body?
          !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
        end

        DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"

        def set_conditional_cache_control!
          control = @cache_control

118 119
          return if self["Cache-Control"].present?

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
          if control.empty?
            headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
          elsif @cache_control[:no_cache]
            headers["Cache-Control"] = "no-cache"
          else
            extras  = control[:extras]
            max_age = control[:max_age]

            options = []
            options << "max-age=#{max_age.to_i}" if max_age
            options << (control[:public] ? "public" : "private")
            options << "must-revalidate" if control[:must_revalidate]
            options.concat(extras) if extras

            headers["Cache-Control"] = options.join(", ")
          end
        end
      end
    end
  end
end