Skip to content

Commit e12b8d8

Browse files
authored
Merge pull request rails#42123 from Shopify/safe-buffer-implicit-casting
Stop implicitly coercing objects to string in Safebuffer
2 parents 99c9036 + 147f207 commit e12b8d8

File tree

5 files changed

+65
-12
lines changed

5 files changed

+65
-12
lines changed

actionview/lib/action_view/helpers/form_helper.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2610,7 +2610,9 @@ def fields_for_with_nested_attributes(association_name, association, options, bl
26102610
else
26112611
options[:child_index] = nested_child_index(name)
26122612
end
2613-
output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
2613+
if content = fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
2614+
output << content
2615+
end
26142616
end
26152617
output
26162618
elsif association

actionview/lib/action_view/helpers/form_tag_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ def form_tag_html(html_options)
923923

924924
def form_tag_with_body(html_options, content)
925925
output = form_tag_html(html_options)
926-
output << content
926+
output << content.to_s if content
927927
output.safe_concat("</form>")
928928
end
929929

activesupport/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Deprecate `ActiveSupport::SafeBuffer`'s incorrect implicit conversion of objects into string.
2+
3+
Except for a few methods like `String#%`, objects must implement `#to_str`
4+
to be implictly converted to a String in string operations. In some
5+
circumstances `ActiveSupport::SafeBuffer` was incorrectly calling the
6+
explicit conversion method (`#to_s`) on them. This behavior is now
7+
deprecated.
8+
9+
*Jean Boussier*
10+
111
* Allow nested access to keys on `Rails.application.credentials`
212

313
Previously only top level keys in `credentials.yml.enc` could be accessed with method calls. Now any key can.

activesupport/lib/active_support/core_ext/string/output_safety.rb

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,27 +184,27 @@ def clone_empty
184184
end
185185

186186
def concat(value)
187-
super(html_escape_interpolated_argument(value))
187+
super(implicit_html_escape_interpolated_argument(value))
188188
end
189189
alias << concat
190190

191191
def insert(index, value)
192-
super(index, html_escape_interpolated_argument(value))
192+
super(index, implicit_html_escape_interpolated_argument(value))
193193
end
194194

195195
def prepend(value)
196-
super(html_escape_interpolated_argument(value))
196+
super(implicit_html_escape_interpolated_argument(value))
197197
end
198198

199199
def replace(value)
200-
super(html_escape_interpolated_argument(value))
200+
super(implicit_html_escape_interpolated_argument(value))
201201
end
202202

203203
def []=(*args)
204204
if args.length == 3
205-
super(args[0], args[1], html_escape_interpolated_argument(args[2]))
205+
super(args[0], args[1], implicit_html_escape_interpolated_argument(args[2]))
206206
else
207-
super(args[0], html_escape_interpolated_argument(args[1]))
207+
super(args[0], implicit_html_escape_interpolated_argument(args[1]))
208208
end
209209
end
210210

@@ -222,9 +222,9 @@ def *(*)
222222
def %(args)
223223
case args
224224
when Hash
225-
escaped_args = args.transform_values { |arg| html_escape_interpolated_argument(arg) }
225+
escaped_args = args.transform_values { |arg| explicit_html_escape_interpolated_argument(arg) }
226226
else
227-
escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
227+
escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) }
228228
end
229229

230230
self.class.new(super(escaped_args))
@@ -289,10 +289,33 @@ def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
289289
end
290290

291291
private
292-
def html_escape_interpolated_argument(arg)
292+
def explicit_html_escape_interpolated_argument(arg)
293293
(!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
294294
end
295295

296+
def implicit_html_escape_interpolated_argument(arg)
297+
if !html_safe? || arg.html_safe?
298+
arg
299+
else
300+
arg_string = begin
301+
arg.to_str
302+
rescue NoMethodError => error
303+
if error.name == :to_str
304+
str = arg.to_s
305+
ActiveSupport::Deprecation.warn <<~MSG.squish
306+
Implicit conversion of #{arg.class} into String by ActiveSupport::SafeBuffer
307+
is deprecated and will be removed in Rails 7.1.
308+
You must explicitly cast it to a String.
309+
MSG
310+
str
311+
else
312+
raise
313+
end
314+
end
315+
CGI.escapeHTML(arg_string)
316+
end
317+
end
318+
296319
def set_block_back_references(block, match_data)
297320
block.binding.eval("proc { |m| $~ = m }").call(match_data)
298321
rescue ArgumentError

activesupport/test/core_ext/string_ext_test.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,10 +786,15 @@ class OutputSafetyTest < ActiveSupport::TestCase
786786
def setup
787787
@string = +"hello"
788788
@object = Class.new(Object) do
789-
def to_s
789+
def to_str
790790
"other"
791791
end
792792
end.new
793+
@to_s_object = Class.new(Object) do
794+
def to_s
795+
"to_s"
796+
end
797+
end.new
793798
end
794799

795800
test "A string is unsafe by default" do
@@ -817,6 +822,14 @@ def to_s
817822
assert_not_predicate @object, :html_safe?
818823
end
819824

825+
test "Adding an object not responding to `#to_str` to a safe string is deprecated" do
826+
string = @string.html_safe
827+
assert_deprecated("Implicit conversion of #{@to_s_object.class} into String by ActiveSupport::SafeBuffer is deprecated") do
828+
string << @to_s_object
829+
end
830+
assert_equal "helloto_s", string
831+
end
832+
820833
test "Adding an object to a safe string returns a safe string" do
821834
string = @string.html_safe
822835
string << @object
@@ -913,6 +926,11 @@ def to_s
913926
assert_not_predicate @other_string, :html_safe?
914927
end
915928

929+
test "% method explicitly cast the argument to string" do
930+
@other_string = "other%s"
931+
assert_equal "otherto_s", @other_string % @to_s_object
932+
end
933+
916934
test "Concatting unsafe onto safe with % yields escaped safe" do
917935
@other_string = "other%s".html_safe
918936
string = @other_string % "<foo>"

0 commit comments

Comments
 (0)