Skip to content

Commit 0ea26b5

Browse files
committed
Fix double escaping in attributes, <script> and <style> tags
1 parent 89296bf commit 0ea26b5

61 files changed

Lines changed: 1521 additions & 166 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/force_update_snapshots

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
set -e # Exit on error
4+
5+
FORCE_UPDATE_SNAPSHOTS=true bundle exec rake test
6+
7+
echo ""
8+
echo "Force updated snapshots!"

bin/update_snapshots

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
set -e # Exit on error
4+
5+
UPDATE_SNAPSHOTS=true bundle exec rake test
6+
7+
echo ""
8+
echo "Updated snapshots!"

lib/reactionview/template/handlers/herb/herb.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ def initialize(input, properties = {})
2020
# Tell Herb whether the template will be compiled with `frozen_string_literal: true`
2121
properties[:freeze_template_literals] = !::ActionView::Template.frozen_string_literal
2222

23+
# Disable all Herb escape functions - let ActionView::OutputBuffer handle escaping
2324
properties[:escapefunc] = ""
25+
properties[:attrfunc] = ""
26+
properties[:jsfunc] = ""
27+
properties[:cssfunc] = ""
2428

2529
super
2630
end

test/snapshot_utils.rb

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# frozen_string_literal: true
2+
3+
# This is a copy of the SnapshotUtils from Herb:
4+
# https://github.com/marcoroth/herb/blob/638e09f894e4473b661ec6d345d84f5c4d17aa74/test/snapshot_utils.rb
5+
6+
require "fileutils"
7+
require "readline"
8+
require "digest"
9+
10+
def ask?(prompt = "")
11+
Readline.readline("===> #{prompt}? (y/N) ", true).squeeze(" ").strip == "y"
12+
end
13+
14+
module SnapshotUtils
15+
def assert_compiled_snapshot(source, handler: ReActionView::Template::Handlers::ERB, virtual_path: "test", format: :html, locals: [], options: {})
16+
template = ActionView::Template.new(
17+
source,
18+
"test_template",
19+
handler,
20+
virtual_path: virtual_path,
21+
format: format,
22+
locals: locals
23+
)
24+
25+
compiled_source = template.handler.call(template, source)
26+
27+
snapshot_key = {
28+
source: source,
29+
handler: handler,
30+
virtual_path: virtual_path,
31+
options: options,
32+
locals: locals,
33+
format: format,
34+
}.to_s
35+
36+
assert_snapshot_matches(compiled_source, snapshot_key, mode: "compiled")
37+
38+
compiled_source
39+
end
40+
41+
def assert_evaluated_snapshot(source, ivars: {}, options: {}, handler: ReActionView::Template::Handlers::ERB, virtual_path: "test", format: :html, locals: [])
42+
template = ActionView::Template.new(
43+
source,
44+
"test_template",
45+
handler,
46+
virtual_path: virtual_path,
47+
format: format,
48+
locals: locals
49+
)
50+
51+
compiled_source = template.handler.call(template, source)
52+
53+
lookup_context = ActionView::LookupContext.new([])
54+
view_context = ActionView::Base.with_empty_template_cache.new(lookup_context, {}, nil)
55+
56+
ivars.each do |key, value|
57+
view_context.instance_variable_set(:"@#{key}", value)
58+
end
59+
60+
result = view_context.instance_eval(compiled_source).to_s
61+
62+
snapshot_key = {
63+
source: source,
64+
ivars: ivars,
65+
locals: locals,
66+
options: options,
67+
handler: handler,
68+
format: format
69+
}.to_s
70+
71+
assert_snapshot_matches(result, snapshot_key, mode: "evaluated")
72+
73+
{ compiled: compiled_source, result: result }
74+
end
75+
76+
def snapshot_changed?(content, source, options = {})
77+
if snapshot_file(source, options).exist?
78+
previous_content = snapshot_file(source, options).read
79+
80+
if previous_content == content
81+
puts "\n\nSnapshot for '#{class_name} #{name}' didn't change: \n#{snapshot_file(source, options)}\n"
82+
false
83+
else
84+
puts "\n\nSnapshot for '#{class_name} #{name}' changed:\n"
85+
86+
puts Difftastic::Differ.new(color: :always).diff_strings(previous_content, content)
87+
puts "==============="
88+
true
89+
end
90+
else
91+
puts "\n\nSnapshot for '#{class_name} #{name}' doesn't exist at: \n#{snapshot_file(source, options)}\n"
92+
true
93+
end
94+
end
95+
96+
def save_failures_to_snapshot(content, source, options = {})
97+
return unless snapshot_changed?(content, source, options)
98+
99+
puts "\n==== [ Input for '#{class_name} #{name}' ] ====="
100+
puts source
101+
puts "\n\n"
102+
103+
if !ENV["FORCE_UPDATE_SNAPSHOTS"].nil? ||
104+
ask?("Do you want to update (or create) the snapshot for '#{class_name} #{name}'?")
105+
106+
puts "\nUpdating Snapshot for '#{class_name} #{name}' at: \n#{snapshot_file(source, options)}\n"
107+
108+
FileUtils.mkdir_p(snapshot_file(source, options).dirname)
109+
snapshot_file(source, options).write(content)
110+
111+
puts "\nSnapshot for '#{class_name} #{name}' written: \n#{snapshot_file(source, options)}\n"
112+
else
113+
puts "\nNot updating snapshot for '#{class_name} #{name}' at: \n#{snapshot_file(source, options)}.\n"
114+
end
115+
end
116+
117+
def assert_snapshot_matches(actual, source, options = {}, mode: nil)
118+
snapshot_opts = options.dup
119+
snapshot_opts[:mode] = mode if mode
120+
121+
assert snapshot_file(source, snapshot_opts).exist?,
122+
"Expected snapshot file to exist: \n#{snapshot_file(source, snapshot_opts).to_path}"
123+
124+
assert_equal snapshot_file(source, snapshot_opts).read, actual
125+
rescue Minitest::Assertion => e
126+
save_failures_to_snapshot(actual, source, snapshot_opts) if ENV["UPDATE_SNAPSHOTS"] || ENV["FORCE_UPDATE_SNAPSHOTS"]
127+
128+
raise unless snapshot_file(source, snapshot_opts).exist?
129+
130+
if snapshot_file(source, snapshot_opts)&.read != actual
131+
puts
132+
133+
divider = "=" * `tput cols`.strip.to_i
134+
135+
flunk(<<~MESSAGE)
136+
\e[0m
137+
#{divider}
138+
#{Difftastic::Differ.new(color: :always).diff_strings(snapshot_file(source, snapshot_opts).read, actual)}
139+
\e[31m#{divider}
140+
141+
Snapshots for "#{class_name} #{name}" didn't match.
142+
143+
Run the test using UPDATE_SNAPSHOTS=true to update (or create) the snapshot file for "#{class_name} #{name}"
144+
145+
UPDATE_SNAPSHOTS=true mtest #{e.location}
146+
147+
#{divider}
148+
\e[0m
149+
MESSAGE
150+
end
151+
end
152+
153+
def snapshot_file(source, options = {})
154+
test_class_name = underscore(self.class.name)
155+
156+
content_hash = Digest::MD5.hexdigest(source || "#{source.class}-#{source.inspect}")
157+
158+
test_name = sanitize_name_for_filesystem(name)
159+
160+
mode = options[:mode]
161+
mode_suffix = mode ? "_#{mode}" : ""
162+
163+
opts_for_hash = options.reject { |k, _v| k == :mode }
164+
165+
if opts_for_hash && !opts_for_hash.empty?
166+
options_hash = Digest::MD5.hexdigest(opts_for_hash.inspect)
167+
expected_snapshot_filename = "#{test_name}#{mode_suffix}_#{content_hash}-#{options_hash}.txt"
168+
else
169+
expected_snapshot_filename = "#{test_name}#{mode_suffix}_#{content_hash}.txt"
170+
end
171+
172+
base_path = Pathname.new("test/snapshots/") / test_class_name
173+
expected_snapshot_path = base_path / expected_snapshot_filename
174+
175+
return expected_snapshot_path if expected_snapshot_path.exist?
176+
177+
matching_md5_files = if opts_for_hash && !opts_for_hash.empty?
178+
Dir[base_path / "*#{mode_suffix}_#{content_hash}-#{options_hash}.txt"]
179+
else
180+
Dir[base_path / "*#{mode_suffix}_#{content_hash}.txt"]
181+
end
182+
183+
if matching_md5_files.any? && matching_md5_files.length == 1
184+
old_file = Pathname.new(matching_md5_files.first)
185+
186+
return expected_snapshot_path if old_file.rename(expected_snapshot_path).zero?
187+
188+
return old_file
189+
end
190+
191+
expected_snapshot_path
192+
end
193+
194+
private
195+
196+
def sanitize_name_for_filesystem(name)
197+
[
198+
# ntfs reserved characters
199+
# https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
200+
["<", "lt"],
201+
[">", "gt"],
202+
[":", ""],
203+
["/", "_"],
204+
["\\", ""],
205+
["|", ""],
206+
["?", ""],
207+
["*", ""],
208+
209+
[" ", "_"]
210+
].inject(name) { |name, substitution| name.gsub(substitution[0], substitution[1]) }
211+
end
212+
213+
def underscore(string)
214+
string.gsub("::", "/")
215+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
216+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
217+
.tr("-", "_")
218+
.tr(" ", "_")
219+
.downcase
220+
end
221+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@output_buffer.safe_append='Plain text: '.freeze; @output_buffer.safe_expr_append=( 1 + 1 ); @output_buffer.safe_append=' (<with_an_invalid_bracket>)'.freeze;
2+
@output_buffer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Plain text: 2 (<with_an_invalid_bracket>)

0 commit comments

Comments
 (0)