aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/json/encoding.rb
blob: 060dcb6995ed84db4d9c0659231bacc22adabe2f (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
require 'active_support/core_ext/object/json'
require 'active_support/core_ext/module/delegation'

module ActiveSupport
  class << self
    delegate :use_standard_json_time_format, :use_standard_json_time_format=,
      :escape_html_entities_in_json, :escape_html_entities_in_json=,
      :encode_big_decimal_as_string, :encode_big_decimal_as_string=,
      :json_encoder, :json_encoder=,
      :to => :'ActiveSupport::JSON::Encoding'
  end

  module JSON
    # Dumps objects in JSON (JavaScript Object Notation).
    # See www.json.org for more info.
    #
    #   ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
    #   # => "{\"team\":\"rails\",\"players\":\"36\"}"
    def self.encode(value, options = nil)
      Encoding.json_encoder.new(options).encode(value)
    end

    module Encoding #:nodoc:
      class JSONGemEncoder #:nodoc:
        attr_reader :options

        def initialize(options = nil)
          @options = options || {}
        end

        # Encode the given object into a JSON string
        def encode(value)
          stringify jsonify value.as_json(options.dup)
        end

        private
          # Rails does more escaping than the JSON gem natively does (we
          # escape \u2028 and \u2029 and optionally >, <, & to work around
          # certain browser problems).
          ESCAPED_CHARS = {
            "\u2028" => '\u2028',
            "\u2029" => '\u2029',
            '>'      => '\u003e',
            '<'      => '\u003c',
            '&'      => '\u0026',
            }

          ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
          ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u

          # This class wraps all the strings we see and does the extra escaping
          class EscapedString < String #:nodoc:
            def to_json(*)
              if Encoding.escape_html_entities_in_json
                super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
              else
                super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
              end
            end
          end

          # Mark these as private so we don't leak encoding-specific constructs
          private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, 
            :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString

          # Recursively turn the given object into a "jsonified" Ruby data structure
          # that the JSON gem understands - i.e. we want only Hash, Array, String,
          # Numeric, true, false and nil in the final tree. Calls #as_json on it if
          # it's not from one of these base types.
          # 
          # This allows developers to implement #as_json withouth having to worry
          # about what base types of objects they are allowed to return and having
          # to remember calling #as_json recursively.
          # 
          # By default, the options hash is not passed to the children data structures
          # to avoid undesiarable result. Develoers must opt-in by implementing
          # custom #as_json methods (e.g. Hash#as_json and Array#as_json).
          def jsonify(value)
            if value.is_a?(Hash)
              Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
            elsif value.is_a?(Array)
              value.map { |v| jsonify(v) }
            elsif value.is_a?(String)
              EscapedString.new(value)
            elsif value.is_a?(Numeric)
              value
            elsif value == true
              true
            elsif value == false
              false
            elsif value == nil
              nil
            else
              jsonify value.as_json
            end
          end

          # Encode a "jsonified" Ruby data structure using the JSON gem
          def stringify(jsonified)
            ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
          end
      end

      class << self
        # If true, use ISO 8601 format for dates and times. Otherwise, fall back
        # to the Active Support legacy format.
        attr_accessor :use_standard_json_time_format

        # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e)
        # as a safety measure.
        attr_accessor :escape_html_entities_in_json

        # Sets the encoder used by Rails to encode Ruby objects into JSON strings
        # in +Object#to_json+ and +ActiveSupport::JSON.encode+.
        attr_accessor :json_encoder

        def encode_big_decimal_as_string=(as_string)
          message = \
            "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \
            "the new encoder will always encode them as strings.\n\n" \
            "You are seeing this error because you have 'active_support.encode_big_decimal_as_string' in " \
            "your configuration file. If you have been setting this to true, you can safely remove it from " \
            "your configuration. Otherwise, you should add the 'activesupport-json_encoder' gem to your " \
            "Gemfile in order to restore this functionality."

          raise NotImplementedError, message
        end

        def encode_big_decimal_as_string
          message = \
            "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \
            "the new encoder will always encode them as strings.\n\n" \
            "You are seeing this error because you are trying to check the value of the related configuration, " \
            "'active_support.encode_big_decimal_as_string'. If your application depends on this option, you should " \
            "add the 'activesupport-json_encoder' gem to your Gemfile. For now, this option will always be true. " \
            "In the future, it will be removed from Rails, so you should stop checking its value."

          ActiveSupport::Deprecation.warn message

          true
        end

        # Deprecate CircularReferenceError
        def const_missing(name)
          if name == :CircularReferenceError
            message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \
                      "You are seeing this warning because you are rescuing from (or otherwise referencing) " \
                      "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \
                      "removed from Rails. You should remove these rescue blocks from your code and ensure " \
                      "that your data structures are free of circular references so they can be properly " \
                      "serialized into JSON.\n\n" \
                      "For example, the following Hash contains a circular reference to itself:\n" \
                      "   h = {}\n" \
                      "   h['circular'] = h\n" \
                      "In this case, calling h.to_json would not work properly."

            ActiveSupport::Deprecation.warn message

            SystemStackError
          else
            super
          end
        end
      end

      self.use_standard_json_time_format = true
      self.escape_html_entities_in_json  = true
      self.json_encoder = JSONGemEncoder
    end
  end
end