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 63 64 65
    def status=(status)
      @status = status.to_i
    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
      StatusCodes::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 assign_default_content_type_and_charset!
Y
Yehuda Katz 已提交
149
      return if headers[CONTENT_TYPE].present?
150 151

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

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

157
      headers[CONTENT_TYPE] = type
158 159
    end

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

168 169
    alias prepare! to_a

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

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

    def write(str)
183 184
      str = str.to_s
      @writer.call str
185 186 187
      str
    end

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

Y
Yehuda Katz 已提交
205 206 207 208 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
    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

236
    private
237
      def handle_conditional_get!
Y
Yehuda Katz 已提交
238
        if etag? || last_modified? || !@cache_control.empty?
239 240
          set_conditional_cache_control!
        elsif nonempty_ok_response?
241
          self.etag = @body
242 243

          if request && request.etag_matches?(etag)
244
            self.status = 304
245
            self.body = []
246 247 248
          end

          set_conditional_cache_control!
249 250
        else
          headers["Cache-Control"] = "no-cache"
251
        end
252 253
      end

254
      def nonempty_ok_response?
Y
Yehuda Katz 已提交
255
        @status == 200 && string_body?
J
Jeremy Kemper 已提交
256 257 258
      end

      def string_body?
Y
Yehuda Katz 已提交
259
        !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
260 261
      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

284
      end
D
Initial  
David Heinemeier Hansson 已提交
285
  end
286
end