better_html 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +30 -0
- data/lib/better_html.rb +53 -0
- data/lib/better_html/better_erb.rb +68 -0
- data/lib/better_html/better_erb/erubi_implementation.rb +50 -0
- data/lib/better_html/better_erb/erubis_implementation.rb +44 -0
- data/lib/better_html/better_erb/runtime_checks.rb +161 -0
- data/lib/better_html/better_erb/validated_output_buffer.rb +166 -0
- data/lib/better_html/errors.rb +22 -0
- data/lib/better_html/helpers.rb +5 -0
- data/lib/better_html/html_attributes.rb +26 -0
- data/lib/better_html/node_iterator.rb +144 -0
- data/lib/better_html/node_iterator/attribute.rb +34 -0
- data/lib/better_html/node_iterator/base.rb +27 -0
- data/lib/better_html/node_iterator/cdata.rb +8 -0
- data/lib/better_html/node_iterator/comment.rb +8 -0
- data/lib/better_html/node_iterator/content_node.rb +13 -0
- data/lib/better_html/node_iterator/element.rb +26 -0
- data/lib/better_html/node_iterator/html_erb.rb +78 -0
- data/lib/better_html/node_iterator/html_lodash.rb +101 -0
- data/lib/better_html/node_iterator/javascript_erb.rb +60 -0
- data/lib/better_html/node_iterator/location.rb +14 -0
- data/lib/better_html/node_iterator/text.rb +8 -0
- data/lib/better_html/node_iterator/token.rb +8 -0
- data/lib/better_html/railtie.rb +7 -0
- data/lib/better_html/test_helper/ruby_expr.rb +89 -0
- data/lib/better_html/test_helper/safe_erb_tester.rb +202 -0
- data/lib/better_html/test_helper/safe_lodash_tester.rb +121 -0
- data/lib/better_html/test_helper/safety_tester_base.rb +34 -0
- data/lib/better_html/tree.rb +113 -0
- data/lib/better_html/version.rb +3 -0
- data/lib/tasks/better_html_tasks.rake +4 -0
- data/test/better_html/better_erb/implementation_test.rb +402 -0
- data/test/better_html/helpers_test.rb +49 -0
- data/test/better_html/node_iterator/html_lodash_test.rb +132 -0
- data/test/better_html/node_iterator_test.rb +221 -0
- data/test/better_html/test_helper/ruby_expr_test.rb +206 -0
- data/test/better_html/test_helper/safe_erb_tester_test.rb +358 -0
- data/test/better_html/test_helper/safe_lodash_tester_test.rb +80 -0
- data/test/better_html/tree_test.rb +110 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/test_helper.rb +19 -0
- 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
|
data/lib/better_html.rb
ADDED
@@ -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
|