aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/response.rb
blob: da352b69938882b9acfa52c9366e734179d1032e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
require 'digest/md5'

module ActionController # :nodoc:
  # Represents an HTTP response generated by a controller action. One can use an
  # ActionController::AbstractResponse object to retrieve the current state of the
  # response, or customize the response. An AbstractResponse 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.
  #
  # AbstractResponse 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
  # AbstractResponse#headers.
  #
  # Nevertheless, integration tests may want to inspect controller responses in more
  # detail, and that's when AbstractResponse can be useful for application developers.
  # Integration test methods such as ActionController::Integration::Session#get and
  # ActionController::Integration::Session#post return objects of type TestResponse
  # (which are of course also of type AbstractResponse).
  #
  # For example, the following demo integration "test" prints the body of the
  # controller response to the console:
  #
  #  class DemoControllerTest < ActionController::IntegrationTest
  #    def test_print_root_path_to_console
  #      get('/')
  #      puts @response.body
  #    end
  #  end
  class AbstractResponse
    DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
    attr_accessor :request

    # The body content (e.g. HTML) of the response, as a String.
    attr_accessor :body
    # The headers of the response, as a Hash. It maps header names to header values.
    attr_accessor :headers
    attr_accessor :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params, :layout

    def initialize
      @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
    end

    # 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.
    def content_type=(mime_type)
      self.headers["Content-Type"] = charset ? "#{mime_type}; charset=#{charset}" : mime_type
    end
    
    # Returns the response's content MIME type, or nil if content type has been set.
    def content_type
      content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0]
      content_type.blank? ? nil : content_type
    end
    
    def charset=(encoding)
      self.headers["Content-Type"] = "#{content_type || Mime::HTML}; charset=#{encoding}"
    end
    
    def charset
      charset = String(headers["Content-Type"] || headers["type"]).split(";")[1]
      charset.blank? ? nil : charset.strip.split("=")[1]
    end

    def redirect(to_url, response_status)
      self.headers["Status"] = response_status
      self.headers["Location"] = to_url

      self.body = "<html><body>You are being <a href=\"#{to_url}\">redirected</a>.</body></html>"
    end

    def prepare!
      handle_conditional_get!
      convert_content_type!
      set_content_length!
    end

    # Sets the Last-Modified response header. Returns whether it's older than
    # the If-Modified-Since request header.
    def last_modified!(utc_time)
      headers['Last-Modified'] ||= utc_time.httpdate
      if request && since = request.headers['HTTP_IF_MODIFIED_SINCE']
        utc_time <= Time.rfc2822(since)
      end
    end

    # Sets the ETag response header. Returns whether it matches the
    # If-None-Match request header.
    def etag!(tag)
      headers['ETag'] ||= %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(tag))}")
      if request && request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
        true
      end
    end

    private
      def handle_conditional_get!
        if nonempty_ok_response?
          set_conditional_cache_control!

          if etag!(body)
            headers['Status'] = '304 Not Modified'
            self.body = ''
          end
        end
      end

      def nonempty_ok_response?
        status = headers['Status']
        ok = !status || status[0..2] == '200'
        ok && body.is_a?(String) && !body.empty?
      end

      def set_conditional_cache_control!
        if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
          headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
        end
      end

      def convert_content_type!
        if content_type = headers.delete("Content-Type")
          self.headers["type"] = content_type
        end
        if content_type = headers.delete("Content-type")
          self.headers["type"] = content_type
        end
        if content_type = headers.delete("content-type")
          self.headers["type"] = content_type
        end
      end
    
      # Don't set the Content-Length for block-based bodies as that would mean reading it all into memory. Not nice
      # for, say, a 2GB streaming file.
      def set_content_length!
        self.headers["Content-Length"] = body.size unless body.respond_to?(:call)
      end
  end
end