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 52 53 54
      @sending_file = false

      yield self if block_given?
    end

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

59 60 61 62
    def status=(status)
      @status = status.to_i
    end

63 64
    # The response code of the request
    def response_code
65
      @status
66 67 68 69
    end

    # Returns a String to ensure compatibility with Net::HTTPResponse
    def code
70
      @status.to_s
71 72 73
    end

    def message
74
      StatusCodes::STATUS_CODES[@status]
75
    end
76
    alias_method :status_message, :message
77

78 79 80 81 82
    def body
      str = ''
      each { |part| str << part.to_s }
      str
    end
83

Y
Yehuda Katz 已提交
84 85
    EMPTY = " "

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

    def body_parts
      @body
D
Initial  
David Heinemeier Hansson 已提交
93 94
    end

95 96 97 98
    def location
      headers['Location']
    end
    alias_method :redirect_url, :location
99

100 101 102
    def location=(url)
      headers['Location'] = url
    end
103

P
Pratik Naik 已提交
104 105 106 107 108 109 110 111
    # 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.
112
    attr_accessor :charset, :content_type
113

114
    def last_modified
115 116 117 118 119 120 121
      if last = headers['Last-Modified']
        Time.httpdate(last)
      end
    end

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

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

128
    def etag
Y
Yehuda Katz 已提交
129
      @etag
130
    end
131

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

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

141 142 143
    CONTENT_TYPE    = "Content-Type"

    cattr_accessor(:default_charset) { "utf-8" }
144 145

    def assign_default_content_type_and_charset!
Y
Yehuda Katz 已提交
146
      return if headers[CONTENT_TYPE].present?
147 148

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

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

154
      headers[CONTENT_TYPE] = type
155 156
    end

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

165 166
    alias prepare! to_a

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

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

    def write(str)
180 181
      str = str.to_s
      @writer.call str
182 183 184
      str
    end

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

Y
Yehuda Katz 已提交
202 203 204 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
    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

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

          if request && request.etag_matches?(etag)
241
            self.status = 304
242
            self.body = []
243 244 245
          end

          set_conditional_cache_control!
246 247
        else
          headers["Cache-Control"] = "no-cache"
248
        end
249 250
      end

251
      def nonempty_ok_response?
Y
Yehuda Katz 已提交
252
        @status == 200 && string_body?
J
Jeremy Kemper 已提交
253 254 255
      end

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

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

261
      def set_conditional_cache_control!
Y
Yehuda Katz 已提交
262
        control = @cache_control
263

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

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

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

281
      end
D
Initial  
David Heinemeier Hansson 已提交
282
  end
283
end