Skip to content

Commit 943a31c

Browse files
etiennebarriebyroot
andcommitted
Stop escaping JS separators in JSON by default
Introduce a new framework default to skip escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON. Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
1 parent d09c4bb commit 943a31c

File tree

6 files changed

+66
-14
lines changed

6 files changed

+66
-14
lines changed

activesupport/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Add `config.active_support.escape_js_separators_in_json`.
2+
3+
Introduce a new framework default to skip escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON.
4+
5+
Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019.
6+
As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
7+
8+
*Étienne Barrié*, *Jean Boussier*
9+
110
* Fix `NameError` when `class_attribute` is defined on instance singleton classes.
211

312
Previously, calling `class_attribute` on an instance's singleton class would raise

activesupport/lib/active_support/json/encoding.rb

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class << self
88
delegate :use_standard_json_time_format, :use_standard_json_time_format=,
99
:time_precision, :time_precision=,
1010
:escape_html_entities_in_json, :escape_html_entities_in_json=,
11+
:escape_js_separators_in_json, :escape_js_separators_in_json=,
1112
:json_encoder, :json_encoder=,
1213
to: :'ActiveSupport::JSON::Encoding'
1314
end
@@ -67,8 +68,9 @@ module Encoding # :nodoc:
6768
"&".b => '\u0026'.b,
6869
}
6970

70-
ESCAPE_REGEX_WITH_HTML_ENTITIES = Regexp.union(*ESCAPED_CHARS.keys)
71-
ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = Regexp.union(U2028, U2029)
71+
HTML_ENTITIES_REGEX = Regexp.union(*(ESCAPED_CHARS.keys - [U2028, U2029]))
72+
FULL_ESCAPE_REGEX = Regexp.union(*ESCAPED_CHARS.keys)
73+
JS_SEPARATORS_REGEX = Regexp.union(U2028, U2029)
7274

7375
class JSONGemEncoder # :nodoc:
7476
attr_reader :options
@@ -86,14 +88,15 @@ def encode(value)
8688

8789
return json unless @options.fetch(:escape, true)
8890

89-
# Rails does more escaping than the JSON gem natively does (we
90-
# escape \u2028 and \u2029 and optionally >, <, & to work around
91-
# certain browser problems).
9291
json.force_encoding(::Encoding::BINARY)
9392
if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
94-
json.gsub!(ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS)
95-
else
96-
json.gsub!(ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS)
93+
if Encoding.escape_js_separators_in_json
94+
json.gsub!(FULL_ESCAPE_REGEX, ESCAPED_CHARS)
95+
else
96+
json.gsub!(HTML_ENTITIES_REGEX, ESCAPED_CHARS)
97+
end
98+
elsif Encoding.escape_js_separators_in_json
99+
json.gsub!(JS_SEPARATORS_REGEX, ESCAPED_CHARS)
97100
end
98101
json.force_encoding(::Encoding::UTF_8)
99102
end
@@ -184,14 +187,15 @@ def encode(value)
184187

185188
return json unless @escape
186189

187-
# Rails does more escaping than the JSON gem natively does (we
188-
# escape \u2028 and \u2029 and optionally >, <, & to work around
189-
# certain browser problems).
190190
json.force_encoding(::Encoding::BINARY)
191191
if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
192-
json.gsub!(ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS)
193-
else
194-
json.gsub!(ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS)
192+
if Encoding.escape_js_separators_in_json
193+
json.gsub!(FULL_ESCAPE_REGEX, ESCAPED_CHARS)
194+
else
195+
json.gsub!(HTML_ENTITIES_REGEX, ESCAPED_CHARS)
196+
end
197+
elsif Encoding.escape_js_separators_in_json
198+
json.gsub!(JS_SEPARATORS_REGEX, ESCAPED_CHARS)
195199
end
196200
json.force_encoding(::Encoding::UTF_8)
197201
end
@@ -207,6 +211,13 @@ class << self
207211
# as a safety measure.
208212
attr_accessor :escape_html_entities_in_json
209213

214+
# If true, encode LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029)
215+
# as escaped unicode sequences ('\u2028' and '\u2029').
216+
# Historically these characters were not valid inside JavaScript strings
217+
# but that changed in ECMAScript 2019. As such it's no longer a concern in
218+
# modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
219+
attr_accessor :escape_js_separators_in_json
220+
210221
# Sets the precision of encoded time values.
211222
# Defaults to 3 (equivalent to millisecond precision)
212223
attr_accessor :time_precision
@@ -232,6 +243,7 @@ def encode_without_escape(value) # :nodoc:
232243

233244
self.use_standard_json_time_format = true
234245
self.escape_html_entities_in_json = true
246+
self.escape_js_separators_in_json = true
235247
self.json_encoder =
236248
if defined?(JSONGemCoderEncoder)
237249
JSONGemCoderEncoder

activesupport/test/json/encoding_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "securerandom"
44
require_relative "../abstract_unit"
55
require "active_support/core_ext/string/inflections"
6+
require "active_support/core_ext/object/with"
67
require "active_support/json"
78
require "active_support/time"
89
require_relative "../time_zone_test_helpers"
@@ -55,6 +56,9 @@ def test_hash_encoding
5556
def test_unicode_escape
5657
assert_equal %{{"\\u2028":"\\u2029"}}, ActiveSupport::JSON.encode("\u2028" => "\u2029")
5758
assert_equal %{{"\u2028":"\u2029"}}, ActiveSupport::JSON.encode({ "\u2028" => "\u2029" }, escape: false)
59+
ActiveSupport::JSON::Encoding.with(escape_js_separators_in_json: false) do
60+
assert_equal %{{"\u2028":"\u2029"}}, ActiveSupport::JSON.encode({ "\u2028" => "\u2029" })
61+
end
5862
end
5963

6064
def test_hash_keys_encoding

guides/source/configuring.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Below are the default values associated with each target version. In cases of co
6565
- [`config.action_view.remove_hidden_field_autocomplete`](#config-action-view-remove-hidden-field-autocomplete): `true`
6666
- [`config.action_view.render_tracker`](#config-action-view-render-tracker): `:ruby`
6767
- [`config.active_record.raise_on_missing_required_finder_order_columns`](#config-active-record-raise-on-missing-required-finder-order-columns): `true`
68+
- [`config.active_support.escape_js_separators_in_json`](#config-active-support-escape-js-separators-in-json): `false`
6869
- [`config.yjit`](#config-yjit): `!Rails.env.local?`
6970

7071
#### Default Values for Target Version 8.0
@@ -3020,6 +3021,20 @@ end
30203021

30213022
Defaults to `nil`, which means the default `ActiveSupport::EventContext` store is used.
30223023

3024+
#### `config.active_support.escape_js_separators_in_json`
3025+
3026+
Specifies whether LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) are escaped when generating JSON.
3027+
3028+
Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019.
3029+
As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
3030+
3031+
The default value depends on the `config.load_defaults` target version:
3032+
3033+
| Starting with version | The default value is |
3034+
| --------------------- | -------------------- |
3035+
| (original) | `true` |
3036+
| 8.1 | `false` |
3037+
30233038
### Configuring Active Job
30243039
30253040
`config.active_job` provides the following configuration options:

railties/lib/rails/application/configuration.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ def load_defaults(target_version)
365365
active_record.raise_on_missing_required_finder_order_columns = true
366366
end
367367

368+
if respond_to?(:active_support)
369+
active_support.escape_js_separators_in_json = false
370+
end
371+
368372
if respond_to?(:action_view)
369373
action_view.render_tracker = :ruby
370374
end

railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_8_1.rb.tt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
#++
2828
# Rails.configuration.action_controller.escape_json_responses = false
2929

30+
###
31+
# Skips escaping LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) in JSON.
32+
#
33+
# Historically these characters were not valid inside JavaScript literal strings but that changed in ECMAScript 2019.
34+
# As such it's no longer a concern in modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
35+
#++
36+
# Rails.configuration.active_support.escape_js_separators_in_json = false
37+
3038
###
3139
# Raises an error when order dependent finder methods (e.g. `#first`, `#second`) are called without `order` values
3240
# on the relation, and the model does not have any order columns (`implicit_order_column`, `query_constraints`, or

0 commit comments

Comments
 (0)