aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/http/mime_negotiation.rb
blob: 6bf4e652d3dde22a0c11c250b992f885a587c830 (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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# frozen_string_literal: true

require "active_support/core_ext/module/attribute_accessors"

module ActionDispatch
  module Http
    module MimeNegotiation
      extend ActiveSupport::Concern

      RESCUABLE_MIME_FORMAT_ERRORS = [
        ActionController::BadRequest,
        ActionDispatch::Http::Parameters::ParseError,
      ]

      included do
        mattr_accessor :ignore_accept_header, default: false
      end

      # The MIME type of the HTTP request, such as Mime[:xml].
      def content_mime_type
        fetch_header("action_dispatch.request.content_type") do |k|
          v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
            Mime::Type.lookup($1.strip.downcase)
          else
            nil
          end
          set_header k, v
        end
      end

      def content_type
        content_mime_type && content_mime_type.to_s
      end

      def has_content_type? # :nodoc:
        get_header "CONTENT_TYPE"
      end

      # Returns the accepted MIME type for the request.
      def accepts
        fetch_header("action_dispatch.request.accepts") do |k|
          header = get_header("HTTP_ACCEPT").to_s.strip

          v = if header.empty?
            [content_mime_type]
          else
            Mime::Type.parse(header)
          end
          set_header k, v
        end
      end

      # Returns the MIME type for the \format used in the request.
      #
      #   GET /posts/5.xml   | request.format => Mime[:xml]
      #   GET /posts/5.xhtml | request.format => Mime[:html]
      #   GET /posts/5       | request.format => Mime[:html] or Mime[:js], or request.accepts.first
      #
      def format(view_path = [])
        formats.first || Mime::NullType.instance
      end

      def formats
        fetch_header("action_dispatch.request.formats") do |k|
          v = if params_readable?
            Array(Mime[parameters[:format]])
          elsif use_accept_header && valid_accept_header
            accepts
          elsif extension_format = format_from_path_extension
            [extension_format]
          elsif xhr?
            [Mime[:js]]
          else
            [Mime[:html]]
          end

          v = v.select do |format|
            format.symbol || format.ref == "*/*"
          end

          set_header k, v
        end
      end

      # Sets the \variant for template.
      def variant=(variant)
        variant = Array(variant)

        if variant.all? { |v| v.is_a?(Symbol) }
          @variant = ActiveSupport::ArrayInquirer.new(variant)
        else
          raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols."
        end
      end

      def variant
        @variant ||= ActiveSupport::ArrayInquirer.new
      end

      # Sets the \format by string extension, which can be used to force custom formats
      # that are not controlled by the extension.
      #
      #   class ApplicationController < ActionController::Base
      #     before_action :adjust_format_for_iphone
      #
      #     private
      #       def adjust_format_for_iphone
      #         request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
      #       end
      #   end
      def format=(extension)
        parameters[:format] = extension.to_s
        set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
      end

      # Sets the \formats by string extensions. This differs from #format= by allowing you
      # to set multiple, ordered formats, which is useful when you want to have a fallback.
      #
      # In this example, the :iphone format will be used if it's available, otherwise it'll fallback
      # to the :html format.
      #
      #   class ApplicationController < ActionController::Base
      #     before_action :adjust_format_for_iphone_with_html_fallback
      #
      #     private
      #       def adjust_format_for_iphone_with_html_fallback
      #         request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
      #       end
      #   end
      def formats=(extensions)
        parameters[:format] = extensions.first.to_s
        set_header "action_dispatch.request.formats", extensions.collect { |extension|
          Mime::Type.lookup_by_extension(extension)
        }
      end

      # Returns the first MIME type that matches the provided array of MIME types.
      def negotiate_mime(order)
        formats.each do |priority|
          if priority == Mime::ALL
            return order.first
          elsif order.include?(priority)
            return priority
          end
        end

        order.include?(Mime::ALL) ? format : nil
      end

      def should_apply_vary_header?
        !params_readable? && use_accept_header && valid_accept_header
      end

      private
        BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/

        def params_readable? # :doc:
          parameters[:format]
        rescue *RESCUABLE_MIME_FORMAT_ERRORS
          false
        end

        def valid_accept_header # :doc:
          (xhr? && (accept.present? || content_mime_type)) ||
            (accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
        end

        def use_accept_header # :doc:
          !self.class.ignore_accept_header
        end

        def format_from_path_extension # :doc:
          path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
          if match = path && path.match(/\.(\w+)\z/)
            Mime[match.captures.first]
          end
        end
    end
  end
end