Skip to content

Commit 812e50b

Browse files
committed
token_list: Guard Stimulus' data-action from multiple escapes
Prior to this commit, chaining more than one `token_list` calls with a [data-action][] attribute value would result in one too many HTML escapes. Additional subsequent calls would compound the problem. For example, the following calls would result in an invalid descriptor that's escaped too many times to be parsed. ```ruby first = "click->controller#action1" second = "click->controller#action2" third = "click->controller#action3" fourth = "click->controller#action4" value = token_list(first, token_list(second, token_list(third))) CGI.unescape_html value.to_s # => "click->controller#action1 click->controller#action2 click->controller#action3 click->controller#action4" ``` By [CGI.unescape_html][] each `String` value before passing it to [token_list][] (which re-escapes the value), we can preserve a lossless concatenation process while also preserving the HTML safety. After this commit, the previous example works as expected: ```ruby first = "click->controller#action1" second = "click->controller#action2" third = "click->controller#action3" fourth = "click->controller#action4" value = token_list(first, token_list(second, token_list(third))) CGI.unescape_html value.to_s # => "click->controller#action1 click->controller#action2 click->controller#action3 click->controller#action4" ``` [unescaping]: https://ruby-doc.org/stdlib-2.5.3/libdoc/cgi/rdoc/CGI/Util.html#method-i-unescape_html [token_list]: https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-token_list [data-action]: https://stimulus.hotwired.dev/reference/actions
1 parent d77c3fa commit 812e50b

File tree

3 files changed

+28
-1
lines changed

3 files changed

+28
-1
lines changed

actionview/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
* Guard `token_list` calls from escaping HTML too often
2+
3+
*Sean Doyle*
4+
15
* `select` can now be called with a single hash containing options and some HTML options
26

37
Previously this would not work as expected:

actionview/lib/action_view/helpers/tag_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ def content_tag(name, content_or_options_with_block = nil, options = nil, escape
363363
# token_list(nil, false, 123, "", "foo", { bar: true })
364364
# # => "123 foo bar"
365365
def token_list(*args)
366-
tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
366+
tokens = build_tag_values(*args).flat_map { |value| CGI.unescape_html(value.to_s).split(/\s+/) }.uniq
367367

368368
safe_join(tokens, " ")
369369
end

actionview/test/template/tag_helper_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,29 @@ def test_token_list_and_class_names
448448
end
449449
end
450450

451+
def test_token_list_and_class_names_returns_an_html_safe_string
452+
assert_predicate token_list("a value"), :html_safe?
453+
assert_predicate class_names("a value"), :html_safe?
454+
end
455+
456+
def test_token_list_and_class_names_only_html_escape_once
457+
[:token_list, :class_names].each do |helper_method|
458+
helper = ->(*arguments) { public_send(helper_method, *arguments) }
459+
460+
tokens = %w[
461+
click->controller#action1
462+
click->controller#action2
463+
click->controller#action3
464+
click->controller#action4
465+
]
466+
467+
token_list = tokens.reduce(&helper)
468+
469+
assert_predicate token_list, :html_safe?
470+
assert_equal tokens, (CGI.unescape_html(token_list)).split
471+
end
472+
end
473+
451474
def test_content_tag_with_data_attributes
452475
assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double&quot;quote&quot;party&quot;">limelight</p>',
453476
content_tag("p", "limelight", data: { number: 1, string: "hello", string_with_quotes: 'double"quote"party"' })

0 commit comments

Comments
 (0)