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
36
    attr_reader :cache_control
P
Pratik Naik 已提交
37

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

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

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

Y
Yehuda Katz 已提交
50
      @body, @cookie = [], []
51 52 53 54 55
      @sending_file = false

      yield self if block_given?
    end

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

60 61 62 63
    def write(str)
      s = str.to_s
      @writer.call s
      str
64
    end
65

66 67 68 69
    def status=(status)
      @status = status.to_i
    end

70 71
    # The response code of the request
    def response_code
72
      @status
73 74 75 76
    end

    # Returns a String to ensure compatibility with Net::HTTPResponse
    def code
77
      @status.to_s
78 79 80
    end

    def message
81
      StatusCodes::STATUS_CODES[@status]
82
    end
83
    alias_method :status_message, :message
84

85 86 87 88 89
    def body
      str = ''
      each { |part| str << part.to_s }
      str
    end
90

Y
Yehuda Katz 已提交
91 92
    EMPTY = " "

93
    def body=(body)
Y
Yehuda Katz 已提交
94
      @blank = true if body == EMPTY
95
      @body = body.respond_to?(:to_str) ? [body] : body
96 97 98 99
    end

    def body_parts
      @body
D
Initial  
David Heinemeier Hansson 已提交
100 101
    end

102 103 104 105
    def location
      headers['Location']
    end
    alias_method :redirect_url, :location
106

107 108 109
    def location=(url)
      headers['Location'] = url
    end
110

P
Pratik Naik 已提交
111 112 113 114 115 116 117 118
    # 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.
119
    attr_accessor :charset, :content_type
120

121
    def last_modified
122 123 124 125 126 127 128
      if last = headers['Last-Modified']
        Time.httpdate(last)
      end
    end

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

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

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

139
    def etag?
Y
Yehuda Katz 已提交
140
      @etag
141
    end
142

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

148 149 150
    CONTENT_TYPE    = "Content-Type"

    cattr_accessor(:default_charset) { "utf-8" }
151 152

    def assign_default_content_type_and_charset!
Y
Yehuda Katz 已提交
153
      return if headers[CONTENT_TYPE].present?
154 155

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

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

161
      headers[CONTENT_TYPE] = type
162 163
    end

164
    def to_a
165
      assign_default_content_type_and_charset!
166
      handle_conditional_get!
Y
Yehuda Katz 已提交
167 168
      self["Set-Cookie"] = @cookie.join("\n")
      self["ETag"]       = @etag if @etag
169
      super
170
    end
171

172 173
    alias prepare! to_a

174 175 176 177 178
    def each(&callback)
      if @body.respond_to?(:call)
        @writer = lambda { |x| callback.call(x) }
        @body.call(self, self)
      else
179
        @body.each { |part| callback.call(part.to_s) }
180 181 182 183 184 185 186
      end

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

    def write(str)
187 188
      str = str.to_s
      @writer.call str
189 190 191
      str
    end

192 193 194 195 196
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
Y
Yehuda Katz 已提交
197
      if header = @cookie
198 199 200 201 202 203 204
        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
205 206 207 208
      end
      cookies
    end

Y
Yehuda Katz 已提交
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    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

240
    private
241
      def handle_conditional_get!
Y
Yehuda Katz 已提交
242
        if etag? || last_modified? || !@cache_control.empty?
243 244
          set_conditional_cache_control!
        elsif nonempty_ok_response?
245
          self.etag = @body
246 247

          if request && request.etag_matches?(etag)
248
            self.status = 304
249
            self.body = []
250 251 252
          end

          set_conditional_cache_control!
253 254
        else
          headers["Cache-Control"] = "no-cache"
255
        end
256 257
      end

258
      def nonempty_ok_response?
Y
Yehuda Katz 已提交
259
        @status == 200 && string_body?
J
Jeremy Kemper 已提交
260 261 262
      end

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

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

268
      def set_conditional_cache_control!
Y
Yehuda Katz 已提交
269
        control = @cache_control
270

271 272 273 274 275 276 277 278 279 280 281
        if control.empty?
          headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
        else
          extras  = control[:extras]
          max_age = control[:max_age]

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

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

286
      end
D
Initial  
David Heinemeier Hansson 已提交
287
  end
288
end