response.rb 7.6 KB
Newer Older
1
require 'digest/md5'
J
Jeremy Kemper 已提交
2
require 'active_support/core_ext/module/delegation'
3

4
module ActionDispatch # :nodoc:
5
  # Represents an HTTP response generated by a controller action. One can use
J
Joshua Peek 已提交
6
  # an ActionDispatch::Response object to retrieve the current state
7 8 9 10
  # of the response, or customize the response. An Response object can
  # either represent a "real" HTTP response (i.e. one that is meant to be sent
  # back to the web browser) or a test response (i.e. one that is generated
  # from integration tests). See CgiResponse and TestResponse, respectively.
P
Pratik Naik 已提交
11
  #
12 13 14 15 16
  # Response is mostly a Ruby on Rails framework implement detail, and
  # should never be used directly in controllers. Controllers should use the
  # methods defined in ActionController::Base instead. For example, if you want
  # to set the HTTP response's content MIME type, then use
  # ActionControllerBase#headers instead of Response#headers.
P
Pratik Naik 已提交
17
  #
18 19 20
  # Nevertheless, integration tests may want to inspect controller responses in
  # more detail, and that's when Response can be useful for application
  # developers. Integration test methods such as
J
Joshua Peek 已提交
21 22
  # ActionDispatch::Integration::Session#get and
  # ActionDispatch::Integration::Session#post return objects of type
23
  # TestResponse (which are of course also of type Response).
P
Pratik Naik 已提交
24 25 26 27
  #
  # For example, the following demo integration "test" prints the body of the
  # controller response to the console:
  #
J
Joshua Peek 已提交
28
  #  class DemoControllerTest < ActionDispatch::IntegrationTest
P
Pratik Naik 已提交
29 30 31 32 33
  #    def test_print_root_path_to_console
  #      get('/')
  #      puts @response.body
  #    end
  #  end
34
  class Response < Rack::Response
Y
Yehuda Katz 已提交
35
    attr_accessor :request, :blank
P
Pratik Naik 已提交
36

37
    attr_writer :header, :sending_file
38 39
    alias_method :headers=, :header=

D
Initial  
David Heinemeier Hansson 已提交
40
    def initialize
41
      @status = 200
Y
Yehuda Katz 已提交
42
      @header = {}
43
      @cache_control = {}
44 45 46 47 48

      @writer = lambda { |x| @body << x }
      @block = nil
      @length = 0

Y
Yehuda Katz 已提交
49
      @body, @cookie = [], []
50 51
      @sending_file = false

J
Joshua Peek 已提交
52 53 54
      @blank = false
      @etag = nil

55 56 57
      yield self if block_given?
    end

Y
Yehuda Katz 已提交
58 59 60 61
    def cache_control
      @cache_control ||= {}
    end

62
    def status=(status)
63
      @status = Rack::Utils.status_code(status)
64 65
    end

66 67
    # The response code of the request
    def response_code
68
      @status
69 70 71 72
    end

    # Returns a String to ensure compatibility with Net::HTTPResponse
    def code
73
      @status.to_s
74 75 76
    end

    def message
77
      Rack::Utils::HTTP_STATUS_CODES[@status]
78
    end
79
    alias_method :status_message, :message
80

81 82 83 84 85
    def body
      str = ''
      each { |part| str << part.to_s }
      str
    end
86

Y
Yehuda Katz 已提交
87 88
    EMPTY = " "

89
    def body=(body)
Y
Yehuda Katz 已提交
90
      @blank = true if body == EMPTY
91
      @body = body.respond_to?(:to_str) ? [body] : body
92 93 94 95
    end

    def body_parts
      @body
D
Initial  
David Heinemeier Hansson 已提交
96 97
    end

98 99 100 101
    def location
      headers['Location']
    end
    alias_method :redirect_url, :location
102

103 104 105
    def location=(url)
      headers['Location'] = url
    end
106

P
Pratik Naik 已提交
107 108 109 110 111 112 113 114
    # Sets the HTTP response's content MIME type. For example, in the controller
    # you could write this:
    #
    #  response.content_type = "text/plain"
    #
    # If a character set has been defined for this response (see charset=) then
    # the character set information will also be included in the content type
    # information.
115
    attr_accessor :charset, :content_type
116

117
    def last_modified
118 119 120 121 122 123 124
      if last = headers['Last-Modified']
        Time.httpdate(last)
      end
    end

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

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

131
    def etag
Y
Yehuda Katz 已提交
132
      @etag
133
    end
134

135
    def etag?
Y
Yehuda Katz 已提交
136
      @etag
137
    end
138

139
    def etag=(etag)
Y
Yehuda Katz 已提交
140 141
      key = ActiveSupport::Cache.expand_cache_key(etag)
      @etag = %("#{Digest::MD5.hexdigest(key)}")
D
Initial  
David Heinemeier Hansson 已提交
142
    end
143

144 145 146
    CONTENT_TYPE    = "Content-Type"

    cattr_accessor(:default_charset) { "utf-8" }
147

148
    def to_a
149
      assign_default_content_type_and_charset!
150
      handle_conditional_get!
Y
Yehuda Katz 已提交
151 152
      self["Set-Cookie"] = @cookie.join("\n")
      self["ETag"]       = @etag if @etag
153
      super
154
    end
155

156 157
    alias prepare! to_a

158 159 160 161 162
    def each(&callback)
      if @body.respond_to?(:call)
        @writer = lambda { |x| callback.call(x) }
        @body.call(self, self)
      else
163
        @body.each { |part| callback.call(part.to_s) }
164 165 166 167 168 169 170
      end

      @writer = callback
      @block.call(self) if @block
    end

    def write(str)
171 172
      str = str.to_s
      @writer.call str
173 174 175
      str
    end

176 177 178 179 180
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
Y
Yehuda Katz 已提交
181
      if header = @cookie
182 183 184 185 186 187 188
        header = header.split("\n") if header.respond_to?(:to_str)
        header.each do |cookie|
          if pair = cookie.split(';').first
            key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
            cookies[key] = value
          end
        end
189 190 191 192
      end
      cookies
    end

Y
Yehuda Katz 已提交
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    def set_cookie(key, value)
      case value
      when Hash
        domain  = "; domain="  + value[:domain]    if value[:domain]
        path    = "; path="    + value[:path]      if value[:path]
        # According to RFC 2109, we need dashes here.
        # N.B.: cgi.rb uses spaces...
        expires = "; expires=" + value[:expires].clone.gmtime.
          strftime("%a, %d-%b-%Y %H:%M:%S GMT")    if value[:expires]
        secure = "; secure"  if value[:secure]
        httponly = "; HttpOnly" if value[:httponly]
        value = value[:value]
      end
      value = [value]  unless Array === value
      cookie = Rack::Utils.escape(key) + "=" +
        value.map { |v| Rack::Utils.escape v }.join("&") +
        "#{domain}#{path}#{expires}#{secure}#{httponly}"

      @cookie << cookie
    end

    def delete_cookie(key, value={})
      @cookie.reject! { |cookie|
        cookie =~ /\A#{Rack::Utils.escape(key)}=/
      }

      set_cookie(key,
                 {:value => '', :path => nil, :domain => nil,
                   :expires => Time.at(0) }.merge(value))
    end

224
    private
225
      def handle_conditional_get!
Y
Yehuda Katz 已提交
226
        if etag? || last_modified? || !@cache_control.empty?
227 228
          set_conditional_cache_control!
        elsif nonempty_ok_response?
229
          self.etag = @body
230 231

          if request && request.etag_matches?(etag)
232
            self.status = 304
233
            self.body = []
234 235 236
          end

          set_conditional_cache_control!
237 238
        else
          headers["Cache-Control"] = "no-cache"
239
        end
240 241
      end

242
      def nonempty_ok_response?
Y
Yehuda Katz 已提交
243
        @status == 200 && string_body?
J
Jeremy Kemper 已提交
244 245 246
      end

      def string_body?
Y
Yehuda Katz 已提交
247
        !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
248 249
      end

J
Joshua Peek 已提交
250 251 252 253 254 255 256 257 258 259 260 261
      def assign_default_content_type_and_charset!
        return if headers[CONTENT_TYPE].present?

        @content_type ||= Mime::HTML
        @charset      ||= self.class.default_charset

        type = @content_type.to_s.dup
        type << "; charset=#{@charset}" unless @sending_file

        headers[CONTENT_TYPE] = type
      end

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

264
      def set_conditional_cache_control!
Y
Yehuda Katz 已提交
265
        control = @cache_control
266

267 268
        if control.empty?
          headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
269 270
        elsif @cache_control[:no_cache]
          headers["Cache-Control"] = "no-cache"
271 272 273 274 275
        else
          extras  = control[:extras]
          max_age = control[:max_age]

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

281 282
          headers["Cache-Control"] = options.join(", ")
        end
283
      end
D
Initial  
David Heinemeier Hansson 已提交
284
  end
285
end