better_html 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Rakefile +30 -0
  4. data/lib/better_html.rb +53 -0
  5. data/lib/better_html/better_erb.rb +68 -0
  6. data/lib/better_html/better_erb/erubi_implementation.rb +50 -0
  7. data/lib/better_html/better_erb/erubis_implementation.rb +44 -0
  8. data/lib/better_html/better_erb/runtime_checks.rb +161 -0
  9. data/lib/better_html/better_erb/validated_output_buffer.rb +166 -0
  10. data/lib/better_html/errors.rb +22 -0
  11. data/lib/better_html/helpers.rb +5 -0
  12. data/lib/better_html/html_attributes.rb +26 -0
  13. data/lib/better_html/node_iterator.rb +144 -0
  14. data/lib/better_html/node_iterator/attribute.rb +34 -0
  15. data/lib/better_html/node_iterator/base.rb +27 -0
  16. data/lib/better_html/node_iterator/cdata.rb +8 -0
  17. data/lib/better_html/node_iterator/comment.rb +8 -0
  18. data/lib/better_html/node_iterator/content_node.rb +13 -0
  19. data/lib/better_html/node_iterator/element.rb +26 -0
  20. data/lib/better_html/node_iterator/html_erb.rb +78 -0
  21. data/lib/better_html/node_iterator/html_lodash.rb +101 -0
  22. data/lib/better_html/node_iterator/javascript_erb.rb +60 -0
  23. data/lib/better_html/node_iterator/location.rb +14 -0
  24. data/lib/better_html/node_iterator/text.rb +8 -0
  25. data/lib/better_html/node_iterator/token.rb +8 -0
  26. data/lib/better_html/railtie.rb +7 -0
  27. data/lib/better_html/test_helper/ruby_expr.rb +89 -0
  28. data/lib/better_html/test_helper/safe_erb_tester.rb +202 -0
  29. data/lib/better_html/test_helper/safe_lodash_tester.rb +121 -0
  30. data/lib/better_html/test_helper/safety_tester_base.rb +34 -0
  31. data/lib/better_html/tree.rb +113 -0
  32. data/lib/better_html/version.rb +3 -0
  33. data/lib/tasks/better_html_tasks.rake +4 -0
  34. data/test/better_html/better_erb/implementation_test.rb +402 -0
  35. data/test/better_html/helpers_test.rb +49 -0
  36. data/test/better_html/node_iterator/html_lodash_test.rb +132 -0
  37. data/test/better_html/node_iterator_test.rb +221 -0
  38. data/test/better_html/test_helper/ruby_expr_test.rb +206 -0
  39. data/test/better_html/test_helper/safe_erb_tester_test.rb +358 -0
  40. data/test/better_html/test_helper/safe_lodash_tester_test.rb +80 -0
  41. data/test/better_html/tree_test.rb +110 -0
  42. data/test/dummy/README.rdoc +28 -0
  43. data/test/dummy/Rakefile +6 -0
  44. data/test/dummy/app/assets/javascripts/application.js +13 -0
  45. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  46. data/test/dummy/app/controllers/application_controller.rb +5 -0
  47. data/test/dummy/app/helpers/application_helper.rb +2 -0
  48. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  49. data/test/dummy/bin/bundle +3 -0
  50. data/test/dummy/bin/rails +4 -0
  51. data/test/dummy/bin/rake +4 -0
  52. data/test/dummy/bin/setup +29 -0
  53. data/test/dummy/config.ru +4 -0
  54. data/test/dummy/config/application.rb +26 -0
  55. data/test/dummy/config/boot.rb +5 -0
  56. data/test/dummy/config/database.yml +25 -0
  57. data/test/dummy/config/environment.rb +5 -0
  58. data/test/dummy/config/environments/development.rb +41 -0
  59. data/test/dummy/config/environments/production.rb +79 -0
  60. data/test/dummy/config/environments/test.rb +42 -0
  61. data/test/dummy/config/initializers/assets.rb +11 -0
  62. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  63. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  64. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  65. data/test/dummy/config/initializers/inflections.rb +16 -0
  66. data/test/dummy/config/initializers/mime_types.rb +4 -0
  67. data/test/dummy/config/initializers/session_store.rb +3 -0
  68. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/test/dummy/config/locales/en.yml +23 -0
  70. data/test/dummy/config/routes.rb +56 -0
  71. data/test/dummy/config/secrets.yml +22 -0
  72. data/test/dummy/public/404.html +67 -0
  73. data/test/dummy/public/422.html +67 -0
  74. data/test/dummy/public/500.html +66 -0
  75. data/test/dummy/public/favicon.ico +0 -0
  76. data/test/test_helper.rb +19 -0
  77. metadata +205 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ec252f7eed899fcb25b28c2bd6de281b0763163b
4
+ data.tar.gz: 19f784180d9754171e1c0efa5c129ea30f5f00de
5
+ SHA512:
6
+ metadata.gz: cfbac115849137341db904e89c6bade3008d95e85a73eda51e4db11aa338384050cb81bb3bf0828528c704dd01469dcd02a071ca8e06e8e0e7138b098ac558ea
7
+ data.tar.gz: 47181e232f471e71de3699fd9a325268d6deafae7a01ae831221d5dcb952f9ab1deea8a04f6a8147b62a566a820762f3abccad4f555f5cf5d982741a1cb1814c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Francois Chagnon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ require "bundler/gem_tasks"
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ require 'rdoc/task'
9
+
10
+ RDoc::Task.new(:rdoc) do |rdoc|
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = 'BetterHtml'
13
+ rdoc.options << '--line-numbers'
14
+ rdoc.rdoc_files.include('README.rdoc')
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ Bundler::GemHelper.install_tasks
19
+
20
+ require 'rake/testtask'
21
+
22
+ Rake::TestTask.new(:test) do |t|
23
+ t.libs << 'lib'
24
+ t.libs << 'test'
25
+ t.pattern = 'test/**/*_test.rb'
26
+ t.verbose = false
27
+ end
28
+
29
+
30
+ task default: :test
@@ -0,0 +1,53 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+
3
+ module BetterHtml
4
+ class Config
5
+ # regex to validate "foo" in "<foo>"
6
+ cattr_accessor :partial_tag_name_pattern
7
+ self.partial_tag_name_pattern = /\A[a-z0-9\-\:]+\z/
8
+
9
+ # regex to validate "bar" in "<foo bar=1>"
10
+ cattr_accessor :partial_attribute_name_pattern
11
+ self.partial_attribute_name_pattern = /\A[a-zA-Z0-9\-\:]+\z/
12
+
13
+ # true if "<foo bar='1'>" is valid syntax
14
+ cattr_accessor :allow_single_quoted_attributes
15
+ self.allow_single_quoted_attributes = false
16
+
17
+ # true if "<foo bar=1>" is valid syntax
18
+ cattr_accessor :allow_unquoted_attributes
19
+ self.allow_unquoted_attributes = false
20
+
21
+ # all methods that return "javascript-safe" strings
22
+ cattr_accessor :javascript_safe_methods
23
+ self.javascript_safe_methods = ['to_json']
24
+
25
+ # name of all html attributes that may contain javascript
26
+ cattr_accessor :javascript_attribute_names
27
+ self.javascript_attribute_names = [/\Aon/i]
28
+
29
+ cattr_accessor :template_exclusion_filter_block
30
+
31
+ def self.template_exclusion_filter(&block)
32
+ self.template_exclusion_filter_block = block
33
+ end
34
+
35
+ cattr_accessor :lodash_safe_javascript_expression
36
+ self.lodash_safe_javascript_expression = [/\AJSON\.stringify\(/]
37
+ end
38
+
39
+ def self.config
40
+ @config ||= Config.new
41
+ yield @config if block_given?
42
+ @config
43
+ end
44
+ end
45
+
46
+ require 'better_html/version'
47
+ require 'better_html/helpers'
48
+ require 'better_html/errors'
49
+ require 'better_html/html_attributes'
50
+ require 'better_html/node_iterator'
51
+ require 'better_html/tree'
52
+
53
+ require 'better_html/railtie' if defined?(Rails)
@@ -0,0 +1,68 @@
1
+ require 'action_view'
2
+ if ActionView.version < Gem::Version.new("5.1")
3
+ require 'better_html/better_erb/erubis_implementation'
4
+ else
5
+ require 'better_html/better_erb/erubi_implementation'
6
+ end
7
+ require 'better_html/better_erb/validated_output_buffer'
8
+
9
+
10
+ class BetterHtml::BetterErb
11
+ cattr_accessor :content_types
12
+ if ActionView.version < Gem::Version.new("5.1")
13
+ self.content_types = {
14
+ 'html.erb' => BetterHtml::BetterErb::ErubisImplementation
15
+ }
16
+ else
17
+ self.content_types = {
18
+ 'html.erb' => BetterHtml::BetterErb::ErubiImplementation
19
+ }
20
+ end
21
+
22
+ def self.prepend!
23
+ ActionView::Template::Handlers::ERB.prepend(ConditionalImplementation)
24
+ end
25
+
26
+ private
27
+
28
+ module ConditionalImplementation
29
+
30
+ def call(template)
31
+ generate(template)
32
+ end
33
+
34
+ private
35
+
36
+ def generate(template)
37
+ # First, convert to BINARY, so in case the encoding is
38
+ # wrong, we can still find an encoding tag
39
+ # (<%# encoding %>) inside the String using a regular
40
+ # expression
41
+
42
+ filename = template.identifier.split("/").last
43
+ exts = filename.split(".")
44
+ exts = exts[1..exts.length].join(".")
45
+ template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
46
+
47
+ erb = template_source.gsub(ActionView::Template::Handlers::ERB::ENCODING_TAG, '')
48
+ encoding = $2
49
+
50
+ erb.force_encoding valid_encoding(template.source.dup, encoding)
51
+
52
+ # Always make sure we return a String in the default_internal
53
+ erb.encode!
54
+
55
+ excluded_template = !!BetterHtml::Config.template_exclusion_filter_block&.call(template.identifier)
56
+ klass = BetterHtml::BetterErb.content_types[exts] unless excluded_template
57
+ klass ||= self.class.erb_implementation
58
+
59
+ generator = klass.new(
60
+ erb,
61
+ :escape => (self.class.escape_whitelist.include? template.type),
62
+ :trim => (self.class.erb_trim_mode == "-")
63
+ )
64
+ generator.validate! if generator.respond_to?(:validate!)
65
+ generator.src
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,50 @@
1
+ require 'action_view'
2
+ require_relative 'runtime_checks'
3
+
4
+ class BetterHtml::BetterErb
5
+ class ErubiImplementation < ActionView::Template::Handlers::ERB::Erubi
6
+ include RuntimeChecks
7
+
8
+ def add_text(text)
9
+ return if text.empty?
10
+
11
+ if text == "\n"
12
+ @parser.parse("\n")
13
+ @newline_pending += 1
14
+ else
15
+ src << "@output_buffer.safe_append='"
16
+ src << "\n" * @newline_pending if @newline_pending > 0
17
+ src << escape_text(text)
18
+ src << "'.freeze;"
19
+
20
+ @parser.parse(text) do |*args|
21
+ check_token(*args)
22
+ end
23
+
24
+ @newline_pending = 0
25
+ end
26
+ end
27
+
28
+ def add_expression(indicator, code)
29
+ if (indicator == "==") || @escape
30
+ add_expr_auto_escaped(src, code, false)
31
+ else
32
+ add_expr_auto_escaped(src, code, true)
33
+ end
34
+ end
35
+
36
+ def add_code(code)
37
+ flush_newline_if_pending(src)
38
+
39
+ block_check(src, "<%#{code}%>")
40
+ @parser.append_placeholder(code)
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ def escape_text(text)
47
+ text.gsub(/['\\]/, '\\\\\&')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ require 'action_view'
2
+ require_relative 'runtime_checks'
3
+
4
+ class BetterHtml::BetterErb
5
+ class ErubisImplementation < ActionView::Template::Handlers::Erubis
6
+ include RuntimeChecks
7
+
8
+ def add_text(src, text)
9
+ return if text.empty?
10
+
11
+ if text == "\n"
12
+ @parser.parse("\n")
13
+ @newline_pending += 1
14
+ else
15
+ src << "@output_buffer.safe_append='"
16
+ src << "\n" * @newline_pending if @newline_pending > 0
17
+ src << escape_text(text)
18
+ src << "'.freeze;"
19
+
20
+ @parser.parse(text) do |*args|
21
+ check_token(*args)
22
+ end
23
+
24
+ @newline_pending = 0
25
+ end
26
+ end
27
+
28
+ def add_expr_literal(src, code)
29
+ add_expr_auto_escaped(src, code, true)
30
+ end
31
+
32
+ def add_expr_escaped(src, code)
33
+ add_expr_auto_escaped(src, code, false)
34
+ end
35
+
36
+ def add_stmt(src, code)
37
+ flush_newline_if_pending(src)
38
+
39
+ block_check(src, "<%#{code}%>")
40
+ @parser.append_placeholder(code)
41
+ super
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,161 @@
1
+ require 'html_tokenizer'
2
+ require 'action_view'
3
+
4
+ class BetterHtml::BetterErb
5
+ module RuntimeChecks
6
+ def initialize(*)
7
+ @parser = HtmlTokenizer::Parser.new
8
+ super
9
+ end
10
+
11
+ def validate!
12
+ check_parser_errors
13
+
14
+ unless @parser.context == :none
15
+ raise BetterHtml::HtmlError, 'Detected an open tag at the end of this document.'
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def class_name
22
+ "BetterHtml::BetterErb::ValidatedOutputBuffer"
23
+ end
24
+
25
+ def wrap_method
26
+ "#{class_name}.wrap"
27
+ end
28
+
29
+ def add_expr_auto_escaped(src, code, auto_escape)
30
+ flush_newline_if_pending(src)
31
+
32
+ src << "#{wrap_method}(@output_buffer, (#{parser_context.inspect}), '#{escape_text(code)}'.freeze, #{auto_escape})"
33
+ method_name = "safe_#{@parser.context}_append"
34
+ if code =~ self.class::BLOCK_EXPR
35
+ block_check(src, "<%=#{code}%>")
36
+ src << ".#{method_name}= " << code
37
+ else
38
+ src << ".#{method_name}=(" << code << ");"
39
+ end
40
+ @parser.append_placeholder("<%=#{code}%>")
41
+ end
42
+
43
+ def parser_context
44
+ if [:quoted_value, :unquoted_value, :space_after_attribute].include?(@parser.context)
45
+ {
46
+ tag_name: @parser.tag_name,
47
+ attribute_name: @parser.attribute_name,
48
+ attribute_value: @parser.attribute_value,
49
+ attribute_quoted: @parser.attribute_quoted?,
50
+ quote_character: @parser.quote_character,
51
+ }
52
+ elsif [:attribute_name, :after_attribute_name, :after_equal].include?(@parser.context)
53
+ {
54
+ tag_name: @parser.tag_name,
55
+ attribute_name: @parser.attribute_name,
56
+ }
57
+ elsif [:tag, :tag_name, :tag_end].include?(@parser.context)
58
+ {
59
+ tag_name: @parser.tag_name,
60
+ }
61
+ elsif @parser.context == :rawtext
62
+ {
63
+ tag_name: @parser.tag_name,
64
+ rawtext_text: @parser.rawtext_text,
65
+ }
66
+ elsif @parser.context == :comment
67
+ {
68
+ comment_text: @parser.comment_text,
69
+ }
70
+ elsif [:none, :solidus_or_tag_name].include?(@parser.context)
71
+ {}
72
+ else
73
+ raise RuntimeError, "Tried to interpolate into unknown location #{@parser.context}."
74
+ end
75
+ end
76
+
77
+ def block_check(src, code)
78
+ unless @parser.context == :none || @parser.context == :rawtext
79
+ s = "Ruby statement not allowed.\n"
80
+ s << "In '#{@parser.context}' on line #{@parser.line_number} column #{@parser.column_number}:\n"
81
+ prefix = extract_line(@parser.line_number)
82
+ code = code.lines.first
83
+ s << "#{prefix}#{code}\n"
84
+ s << "#{' ' * prefix.size}#{'^' * code.size}"
85
+ raise BetterHtml::DontInterpolateHere, s
86
+ end
87
+ end
88
+
89
+ def check_parser_errors
90
+ errors = @parser.errors
91
+ return if errors.empty?
92
+
93
+ s = "#{errors.size} error(s) found in HTML document.\n"
94
+ errors.each do |error|
95
+ s = "#{error.message}\n"
96
+ s << "On line #{error.line} column #{error.column}:\n"
97
+ line = extract_line(error.line)
98
+ s << "#{line}\n"
99
+ s << "#{' ' * (error.column)}#{'^' * (line.size - error.column)}"
100
+ end
101
+
102
+ raise BetterHtml::HtmlError, s
103
+ end
104
+
105
+ def check_token(type, *args)
106
+ check_tag_name(type, *args) if type == :tag_name
107
+ check_attribute_name(type, *args) if type == :attribute_name
108
+ check_quoted_value(type, *args) if type == :attribute_quoted_value_start
109
+ check_unquoted_value(type, *args) if type == :attribute_unquoted_value
110
+ end
111
+
112
+ def check_tag_name(type, start, stop, line, column)
113
+ text = @parser.extract(start, stop)
114
+ return if text.upcase == "!DOCTYPE"
115
+ return if BetterHtml.config.partial_tag_name_pattern === text
116
+
117
+ s = "Invalid tag name #{text.inspect} does not match "\
118
+ "regular expression #{BetterHtml.config.partial_tag_name_pattern.inspect}\n"
119
+ s << build_location(line, column, text.size)
120
+ raise BetterHtml::HtmlError, s
121
+ end
122
+
123
+ def check_attribute_name(type, start, stop, line, column)
124
+ text = @parser.extract(start, stop)
125
+ return if BetterHtml.config.partial_attribute_name_pattern === text
126
+
127
+ s = "Invalid attribute name #{text.inspect} does not match "\
128
+ "regular expression #{BetterHtml.config.partial_attribute_name_pattern.inspect}\n"
129
+ s << build_location(line, column, text.size)
130
+ raise BetterHtml::HtmlError, s
131
+ end
132
+
133
+ def check_quoted_value(type, start, stop, line, column)
134
+ return if BetterHtml.config.allow_single_quoted_attributes
135
+ text = @parser.extract(start, stop)
136
+ return if text == '"'
137
+
138
+ s = "Single-quoted attributes are not allowed\n"
139
+ s << build_location(line, column, text.size)
140
+ raise BetterHtml::HtmlError, s
141
+ end
142
+
143
+ def check_unquoted_value(type, start, stop, line, column)
144
+ return if BetterHtml.config.allow_unquoted_attributes
145
+ s = "Unquoted attribute values are not allowed\n"
146
+ s << build_location(line, column, stop-start)
147
+ raise BetterHtml::HtmlError, s
148
+ end
149
+
150
+ def build_location(line, column, length)
151
+ s = "On line #{line} column #{column}:\n"
152
+ s << "#{extract_line(line)}\n"
153
+ s << "#{' ' * column}#{'^' * length}"
154
+ end
155
+
156
+ def extract_line(line)
157
+ line = @parser.document.lines[line-1]
158
+ line.nil? ? "" : line.gsub(/\n$/, '')
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,166 @@
1
+ module BetterHtml
2
+ class BetterErb
3
+ class ValidatedOutputBuffer
4
+ def self.wrap(output, context, code, auto_escape)
5
+ Context.new(output, context, code, auto_escape)
6
+ end
7
+
8
+ class Context
9
+ def initialize(output, context, code, auto_escape)
10
+ @output = output
11
+ @context = context
12
+ @code = code
13
+ @auto_escape = auto_escape
14
+ end
15
+
16
+ def safe_quoted_value_append=(value)
17
+ return if value.nil?
18
+ value = properly_escaped(value)
19
+
20
+ if value.include?(@context[:quote_character])
21
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
22
+ "into a quoted attribute value. The value cannot contain the character #{@context[:quote_character]}."
23
+ end
24
+
25
+ @output.safe_append= value
26
+ end
27
+
28
+ def safe_unquoted_value_append=(value)
29
+ raise DontInterpolateHere, "Do not interpolate without quotes around this "\
30
+ "attribute value. Instead of "\
31
+ "<#{@context[:tag_name]} #{@context[:attribute_name]}=#{@context[:attribute_value]}<%=#{@code}%>> "\
32
+ "try <#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}<%=#{@code}%>\">."
33
+ end
34
+
35
+ def safe_space_after_attribute_append=(value)
36
+ raise DontInterpolateHere, "Add a space after this attribute value. Instead of "\
37
+ "<#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\"<%=#{@code}%>> "\
38
+ "try <#{@context[:tag_name]} #{@context[:attribute_name]}=\"#{@context[:attribute_value]}\" <%=#{@code}%>>."
39
+ end
40
+
41
+ def safe_attribute_name_append=(value)
42
+ return if value.nil?
43
+ value = value.to_s
44
+
45
+ unless value =~ /\A[a-z0-9\-]*\z/
46
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
47
+ "into a attribute name around '#{@context[:attribute_name]}<%=#{@code}%>'."
48
+ end
49
+
50
+ @output.safe_append= value
51
+ end
52
+
53
+ def safe_after_attribute_name_append=(value)
54
+ return if value.nil?
55
+
56
+ unless value.is_a?(BetterHtml::HtmlAttributes)
57
+ raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. "\
58
+ "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please "\
59
+ "try <#{@context[:tag_name]} <%= html_attributes(attr: value) %>>."
60
+ end
61
+
62
+ @output.safe_append= value.to_s
63
+ end
64
+
65
+ def safe_after_equal_append=(value)
66
+ raise DontInterpolateHere, "Do not interpolate without quotes after "\
67
+ "attribute around '#{@context[:attribute_name]}=<%=#{@code}%>'."
68
+ end
69
+
70
+ def safe_tag_append=(value)
71
+ return if value.nil?
72
+
73
+ unless value.is_a?(BetterHtml::HtmlAttributes)
74
+ raise DontInterpolateHere, "Do not interpolate #{value.class} in a tag. "\
75
+ "Instead of <#{@context[:tag_name]} <%=#{@code}%>> please "\
76
+ "try <#{@context[:tag_name]} <%= html_attributes(attr: value) %>>."
77
+ end
78
+
79
+ @output.safe_append= value.to_s
80
+ end
81
+
82
+ def safe_tag_name_append=(value)
83
+ return if value.nil?
84
+ value = value.to_s
85
+
86
+ unless value =~ /\A[a-z0-9\:\-]*\z/
87
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
88
+ "into a tag name around: <#{@context[:tag_name]}<%=#{@code}%>>."
89
+ end
90
+
91
+ @output.safe_append= value
92
+ end
93
+
94
+ def safe_rawtext_append=(value)
95
+ return if value.nil?
96
+
97
+ value = properly_escaped(value)
98
+
99
+ if @context[:tag_name].downcase == 'script' &&
100
+ (value =~ /<script/i || value =~ /<\/script/i)
101
+ # https://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements
102
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
103
+ "into a script tag around: <#{@context[:tag_name]}>#{@context[:rawtext_text]}<%=#{@code}%>. "\
104
+ "A script tag cannot contain <script or </script anywhere inside of it."
105
+ elsif value =~ /<#{Regexp.escape(@context[:tag_name].downcase)}/i ||
106
+ value =~ /<\/#{Regexp.escape(@context[:tag_name].downcase)}/i
107
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
108
+ "into a #{@context[:tag_name].downcase} tag around: <#{@context[:tag_name]}>#{@context[:rawtext_text]}<%=#{@code}%>."
109
+ end
110
+
111
+ @output.safe_append= value
112
+ end
113
+
114
+ def safe_comment_append=(value)
115
+ return if value.nil?
116
+ value = properly_escaped(value)
117
+
118
+ # in a <!-- ...here --> we disallow -->
119
+ if value =~ /-->/
120
+ raise UnsafeHtmlError, "Detected invalid characters as part of the interpolation "\
121
+ "into a html comment around: <!--#{@context[:comment_text]}<%=#{@code}%>."
122
+ end
123
+
124
+ @output.safe_append= value
125
+ end
126
+
127
+ def safe_none_append=(value)
128
+ return if value.nil?
129
+ @output.safe_append= properly_escaped(value)
130
+ end
131
+
132
+ private
133
+
134
+ def properly_escaped(value)
135
+ if value.is_a?(ValidatedOutputBuffer)
136
+ # in html context, never escape a ValidatedOutputBuffer
137
+ value.to_s
138
+ else
139
+ # in html context, follow auto_escape rule
140
+ if @auto_escape
141
+ auto_escape_html_safe_value(value.to_s)
142
+ else
143
+ value.to_s
144
+ end
145
+ end
146
+ end
147
+
148
+ def auto_escape_html_safe_value(arg)
149
+ arg.html_safe? ? arg : CGI.escapeHTML(arg).html_safe
150
+ end
151
+ end
152
+
153
+ def html_safe?
154
+ true
155
+ end
156
+
157
+ def html_safe
158
+ self.class.new(@output)
159
+ end
160
+
161
+ def to_s
162
+ @output.html_safe
163
+ end
164
+ end
165
+ end
166
+ end