response.rb 6.1 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=

40 41 42 43 44 45 46
    module Setup
      def initialize(status = 200, header = {}, body = [])
        @writer = lambda { |x| @body << x }
        @block = nil
        @length = 0

        @status, @header, @body = status, header, body
47

48 49
        @cookie = []
        @sending_file = false
50

51
        @blank = false
52

53 54 55 56 57
        if content_type = self["Content-Type"]
          type, charset = content_type.split(/;\s*charset=/)
          @content_type = Mime::Type.lookup(type)
          @charset = charset || "UTF-8"
        end
J
Joshua Peek 已提交
58

59 60
        yield self if block_given?
      end
61 62
    end

63 64 65
    include Setup
    include ActionDispatch::Http::Cache::Response

66
    def status=(status)
67
      @status = Rack::Utils.status_code(status)
68 69
    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
      Rack::Utils::HTTP_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 122 123
    CONTENT_TYPE    = "Content-Type"

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

125
    def to_a
126
      assign_default_content_type_and_charset!
127
      handle_conditional_get!
J
Joshua Peek 已提交
128
      self["Set-Cookie"] = @cookie.join("\n") unless @cookie.blank?
129
      self["ETag"]       = @_etag if @_etag
130
      super
131
    end
132

133 134
    alias prepare! to_a

135 136 137 138 139
    def each(&callback)
      if @body.respond_to?(:call)
        @writer = lambda { |x| callback.call(x) }
        @body.call(self, self)
      else
140
        @body.each { |part| callback.call(part.to_s) }
141 142 143 144 145 146 147
      end

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

    def write(str)
148 149
      str = str.to_s
      @writer.call str
150 151 152
      str
    end

153 154 155 156 157
    # Returns the response cookies, converted to a Hash of (name => value) pairs
    #
    #   assert_equal 'AuthorOfNewPage', r.cookies['author']
    def cookies
      cookies = {}
Y
Yehuda Katz 已提交
158
      if header = @cookie
159 160 161 162 163 164 165
        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
166 167 168 169
      end
      cookies
    end

Y
Yehuda Katz 已提交
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
    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

201
    private
J
Joshua Peek 已提交
202 203 204 205 206 207 208 209 210 211 212 213
      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

D
Initial  
David Heinemeier Hansson 已提交
214
  end
215
end