mime_negotiation.rb 5.5 KB
Newer Older
B
Bogdan Gusiev 已提交
1 2
require 'active_support/core_ext/module/attribute_accessors'

3 4 5
module ActionDispatch
  module Http
    module MimeNegotiation
6 7 8 9 10 11 12
      extend ActiveSupport::Concern

      included do
        mattr_accessor :ignore_accept_header
        self.ignore_accept_header = false
      end

13 14 15 16
      # The MIME type of the HTTP request, such as Mime::XML.
      #
      # For backward compatibility, the post \format is extracted from the
      # X-Post-Data-Format HTTP header if present.
17
      def content_mime_type
18 19 20 21 22 23 24 25 26
        @env["action_dispatch.request.content_type"] ||= begin
          if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
            Mime::Type.lookup($1.strip.downcase)
          else
            nil
          end
        end
      end

27 28 29 30
      def content_type
        content_mime_type && content_mime_type.to_s
      end

31 32 33 34 35 36
      # Returns the accepted MIME type for the request.
      def accepts
        @env["action_dispatch.request.accepts"] ||= begin
          header = @env['HTTP_ACCEPT'].to_s.strip

          if header.empty?
37
            [content_mime_type]
38 39 40 41 42 43
          else
            Mime::Type.parse(header)
          end
        end
      end

44
      # Returns the MIME type for the \format used in the request.
45 46 47
      #
      #   GET /posts/5.xml   | request.format => Mime::XML
      #   GET /posts/5.xhtml | request.format => Mime::HTML
48
      #   GET /posts/5       | request.format => Mime::HTML or MIME::JS, or request.accepts.first
49 50
      #
      def format(view_path = [])
51
        formats.first || Mime::NullType.instance
52 53 54
      end

      def formats
J
Jarmo Isotalo 已提交
55 56 57 58 59 60 61 62
        @env["action_dispatch.request.formats"] ||= begin
          params_readable = begin
                              parameters[:format]
                            rescue ActionController::BadRequest
                              false
                            end

          if params_readable
63
            Array(Mime[parameters[:format]])
64
          elsif use_accept_header && valid_accept_header
65
            accepts
66 67
          elsif xhr?
            [Mime::JS]
68 69 70
          else
            [Mime::HTML]
          end
J
Jarmo Isotalo 已提交
71
        end
72
      end
73

74
      # Sets the \variant for template.
Ł
Łukasz Strzałkowski 已提交
75
      def variant=(variant)
76 77 78 79
        variant = Array(variant)

        if variant.all? { |v| v.is_a?(Symbol) }
          @variant = VariantInquirer.new(variant)
Ł
Łukasz Strzałkowski 已提交
80
        else
81
          raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \
82 83 84
            "For security reasons, never directly set the variant to a user-provided value, " \
            "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
            "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
Ł
Łukasz Strzałkowski 已提交
85 86 87
        end
      end

88 89 90 91
      def variant
        @variant ||= VariantInquirer.new
      end

92 93 94 95
      # 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
96
      #     before_action :adjust_format_for_iphone
97 98 99 100 101 102 103 104 105 106 107
      #
      #     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
        @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
      end

108 109 110 111 112 113 114
      # 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
115
      #     before_action :adjust_format_for_iphone_with_html_fallback
116 117 118 119 120 121 122 123 124 125 126 127 128
      #
      #     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
        @env["action_dispatch.request.formats"] = extensions.collect do |extension|
          Mime::Type.lookup_by_extension(extension)
        end
      end

129 130 131 132 133 134 135 136 137 138 139 140
      # Receives an array of mimes and return the first user sent mime that
      # matches the order array.
      #
      def negotiate_mime(order)
        formats.each do |priority|
          if priority == Mime::ALL
            return order.first
          elsif order.include?(priority)
            return priority
          end
        end

141
        order.include?(Mime::ALL) ? format : nil
142
      end
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
      class VariantInquirer # :nodoc:
        delegate :each, :empty?, to: :@variants

        def initialize(variants = [])
          @variants = variants
        end

        def any?(*candidates)
          (@variants & candidates).any?
        end

        def to_ary
          @variants
        end

        private
          def method_missing(name, *args)
            if name[-1] == '?'
              any? name[0..-2].to_sym
            else
              super
            end
          end
      end

169 170 171 172 173
      protected

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

      def valid_accept_header
174
        (xhr? && (accept.present? || content_mime_type)) ||
175
          (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
176 177 178 179 180
      end

      def use_accept_header
        !self.class.ignore_accept_header
      end
181 182
    end
  end
183
end