Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
ff2e6dc1
Commit
ff2e6dc1
authored
Apr 11, 2016
by
Robert Raposa
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12082 from edx/robrap/linter-mako
TNL-4324: Enhance linter for HTML() and Text()
parents
498ab887
96fe8afb
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
461 additions
and
165 deletions
+461
-165
scripts/safe_template_linter.py
+237
-52
scripts/tests/test_safe_template_linter.py
+224
-113
No files found.
scripts/safe_template_linter.py
View file @
ff2e6dc1
...
...
@@ -3,6 +3,7 @@
A linting tool to check if templates are safe
"""
from
__future__
import
print_function
import
argparse
from
enum
import
Enum
import
os
import
re
...
...
@@ -205,8 +206,8 @@ class Rules(Enum):
'mako-multiple-page-tags'
,
'A Mako template can only have one <
%
page> tag.'
)
mako_unparsable_expression
=
(
'mako-unparsable-expression'
,
mako_unpars
e
able_expression
=
(
'mako-unpars
e
able-expression'
,
'The expression could not be properly parsed.'
)
mako_unwanted_html_filter
=
(
...
...
@@ -217,15 +218,30 @@ class Rules(Enum):
'mako-invalid-html-filter'
,
'The expression is using an invalid filter in an HTML context.'
)
mako_invalid_js_filter
=
(
'mako-invalid-js-filter'
,
'The expression is using an invalid filter in a JavaScript context.'
)
mako_deprecated_display_name
=
(
'mako-deprecated-display-name'
,
'Replace deprecated display_name_with_default_escaped with display_name_with_default.'
)
mako_invalid_js_filter
=
(
'mako-invalid-js-filter'
,
'The expression is using an invalid filter in a JavaScript context.'
mako_html_requires_text
=
(
'mako-html-requires-text'
,
'You must begin with Text() if you use HTML() during interpolation.'
)
mako_close_before_format
=
(
'mako-close-before-format'
,
'You must close any call to Text() or HTML() before calling format().'
)
mako_text_redundant
=
(
'mako-text-redundant'
,
'Using Text() function without HTML() is unnecessary.'
)
mako_html_alone
=
(
'mako-html-alone'
,
"Only use HTML() alone with properly escaped HTML(), and make sure it is really alone."
)
underscore_not_escaped
=
(
'underscore-not-escaped'
,
'Expressions should be escaped using <
%-
expression
%
>.'
...
...
@@ -421,7 +437,7 @@ class ExpressionRuleViolation(RuleViolation):
line_number
,
column
,
rule_id
,
self
.
lines
[
line_number
-
self
.
start_line
-
1
]
.
encode
(
encoding
=
'utf-8'
)
self
.
lines
[
line_number
-
self
.
start_line
]
.
encode
(
encoding
=
'utf-8'
)
),
file
=
out
)
...
...
@@ -630,13 +646,14 @@ class MakoTemplateLinter(object):
for
expression
in
expressions
:
if
expression
[
'expression'
]
is
None
:
results
.
violations
.
append
(
ExpressionRuleViolation
(
Rules
.
mako_unparsable_expression
,
expression
Rules
.
mako_unpars
e
able_expression
,
expression
))
continue
context
=
self
.
_get_context
(
contexts
,
expression
[
'start_index'
])
self
.
_check_filters
(
mako_template
,
expression
,
context
,
has_page_default
,
results
)
self
.
_check_deprecated_display_name
(
expression
,
results
)
self
.
_check_html_and_text
(
expression
,
results
)
def
_check_deprecated_display_name
(
self
,
expression
,
results
):
"""
...
...
@@ -654,6 +671,52 @@ class MakoTemplateLinter(object):
Rules
.
mako_deprecated_display_name
,
expression
))
def
_check_html_and_text
(
self
,
expression
,
results
):
"""
Checks rules related to proper use of HTML() and Text().
Rule 1: If HTML() is called, the expression must begin with Text(), or
Rule 2: If HTML() is called alone, it must be the only call.
Rule 3: Both HTML() and Text() must be closed before any call to
format().
Rule 4: Using Text() without HTML() is unnecessary.
Arguments:
expression: A dict containing the start_index, end_index, and
expression (text) of the expression.
results: A list of results into which violations will be added.
"""
# strip '${' and '}' and whitespace from ends
expression_inner
=
expression
[
'expression'
][
2
:
-
1
]
.
strip
()
if
'HTML('
in
expression_inner
:
if
expression_inner
.
startswith
(
'HTML('
):
close_paren_index
=
self
.
_find_closing_char_index
(
None
,
"("
,
")"
,
expression_inner
,
len
(
'HTML('
),
0
)
# check that the close paren is at the end of the expression.
if
close_paren_index
!=
len
(
expression_inner
)
-
1
:
results
.
violations
.
append
(
ExpressionRuleViolation
(
Rules
.
mako_html_alone
,
expression
))
elif
expression_inner
.
startswith
(
'Text('
)
is
False
:
results
.
violations
.
append
(
ExpressionRuleViolation
(
Rules
.
mako_html_requires_text
,
expression
))
else
:
if
'Text('
in
expression_inner
:
results
.
violations
.
append
(
ExpressionRuleViolation
(
Rules
.
mako_text_redundant
,
expression
))
for
match
in
re
.
finditer
(
"(HTML
\
(|Text
\
()"
,
expression_inner
):
close_paren_index
=
self
.
_find_closing_char_index
(
None
,
"("
,
")"
,
expression_inner
,
match
.
end
(),
0
)
if
0
<=
close_paren_index
:
# the argument sent to HTML() or Text()
argument
=
expression_inner
[
match
.
end
():
close_paren_index
]
if
".format("
in
argument
:
results
.
violations
.
append
(
ExpressionRuleViolation
(
Rules
.
mako_close_before_format
,
expression
))
def
_check_filters
(
self
,
mako_template
,
expression
,
context
,
has_page_default
,
results
):
"""
Checks that the filters used in the given Mako expression are valid
...
...
@@ -796,9 +859,9 @@ class MakoTemplateLinter(object):
while
True
:
start_index
=
mako_template
.
find
(
start_delim
,
start_index
)
if
(
start_index
<
0
)
:
if
start_index
<
0
:
break
end_index
=
self
.
_find_
balanced_end_curly
(
mako_template
,
start_index
+
len
(
start_delim
),
0
)
end_index
=
self
.
_find_
closing_char_index
(
start_delim
,
'{'
,
'}'
,
mako_template
,
start_index
+
len
(
start_delim
),
0
)
if
end_index
<
0
:
expression
=
None
...
...
@@ -817,39 +880,117 @@ class MakoTemplateLinter(object):
return
expressions
def
_find_
balanced_end_curly
(
self
,
mako_template
,
start_index
,
num_open_curlie
s
):
def
_find_
closing_char_index
(
self
,
start_delim
,
open_char
,
close_char
,
template
,
start_index
,
num_open_char
s
):
"""
Finds the end index of the Mako expression's ending curly brace. Skips
any additional open/closed braces that are balanced inside. Does not
take into consideration strings.
Finds the index of the closing char that matches the opening char.
For example, this could be used to find the end of a Mako expression,
where the open and close characters would be '{' and '}'.
Arguments:
mako_template: The template text.
start_index: The start index of the Mako expression.
num_open_curlies: The current number of open expressions.
start_delim: If provided (e.g. '${' for Mako expressions), the
closing character must be found before the next start_delim.
open_char: The opening character to be matched (e.g '{')
close_char: The closing character to be matched (e.g '}')
template: The template to be searched.
start_index: The start index of the last open char.
num_open_chars: The current number of open chars.
Returns:
The
end index of the expression
, or -1 if unparseable.
The
index of the closing character
, or -1 if unparseable.
"""
end_curly_index
=
mako_template
.
find
(
'}'
,
start_index
)
if
end_curly
_index
<
0
:
# if we can't find an end_c
urly
, let's just quit
return
end_curly_index
close_char_index
=
template
.
find
(
close_char
,
start_index
)
if
close_char
_index
<
0
:
# if we can't find an end_c
har
, let's just quit
return
-
1
open_curly_index
=
mako_template
.
find
(
'{'
,
start_index
,
end_curly_index
)
open_char_index
=
template
.
find
(
open_char
,
start_index
,
close_char_index
)
start_quote_index
=
self
.
_find_string_start
(
template
,
start_index
,
close_char_index
)
if
(
open_curly_index
>=
0
)
and
(
open_curly_index
<
end_curly_index
):
if
mako_template
[
open_curly_index
-
1
]
==
'$'
:
# assume if we find "${" it is the start of the next expression
# and we have a parse error
if
0
<=
start_quote_index
:
string_end_index
=
self
.
_parse_string
(
template
,
start_quote_index
)[
'end_index'
]
if
string_end_index
<
0
:
return
-
1
else
:
return
self
.
_find_balanced_end_curly
(
mako_template
,
open_curly_index
+
1
,
num_open_curlies
+
1
)
if
num_open_curlies
==
0
:
return
end_curly_index
return
self
.
_find_closing_char_index
(
start_delim
,
open_char
,
close_char
,
template
,
string_end_index
,
num_open_chars
)
if
(
open_char_index
>=
0
)
and
(
open_char_index
<
close_char_index
):
if
start_delim
is
not
None
:
# if we find another starting delim, consider this unparseable
start_delim_index
=
template
.
find
(
start_delim
,
start_index
,
close_char_index
)
if
start_delim_index
<
open_char_index
:
return
-
1
return
self
.
_find_closing_char_index
(
start_delim
,
open_char
,
close_char
,
template
,
open_char_index
+
1
,
num_open_chars
+
1
)
if
num_open_chars
==
0
:
return
close_char_index
else
:
return
self
.
_find_balanced_end_curly
(
mako_template
,
end_curly_index
+
1
,
num_open_curlies
-
1
)
return
self
.
_find_closing_char_index
(
start_delim
,
open_char
,
close_char
,
template
,
close_char_index
+
1
,
num_open_chars
-
1
)
def
_find_string_start
(
self
,
template
,
start_index
,
end_index
):
"""
Finds the index of the end of start of a string. In other words, the
first single or double quote.
Arguments:
template: The template to be searched.
start_index: The start index to search.
end_index: The end index to search before.
num_open_chars: The current number of open expressions.
Returns:
The start index of the first single or double quote, or -1 if
no quote was found.
"""
double_quote_index
=
template
.
find
(
'"'
,
start_index
,
end_index
)
single_quote_index
=
template
.
find
(
"'"
,
start_index
,
end_index
)
if
0
<=
single_quote_index
or
0
<=
double_quote_index
:
if
0
<=
single_quote_index
and
0
<=
double_quote_index
:
return
min
(
single_quote_index
,
double_quote_index
)
else
:
return
max
(
single_quote_index
,
double_quote_index
)
return
-
1
def
_parse_string
(
self
,
template
,
start_index
):
"""
Finds the indices of a string inside a template.
Arguments:
template: The template to be searched.
start_index: The start index of the open quote.
Returns:
A dict containing the following:
start_index: The index of the first quote.
end_index: The index following the closing quote, or -1 if
unparseable
quote_length: The length of the quote. Could be 3 for a Python
triple quote.
"""
quote
=
template
[
start_index
]
if
quote
not
in
[
"'"
,
'"'
]:
raise
ValueError
(
"start_index must refer to a single or double quote."
)
triple_quote
=
quote
*
3
if
template
.
startswith
(
triple_quote
,
start_index
):
quote
=
triple_quote
result
=
{
'start_index'
:
start_index
,
'end_index'
:
-
1
,
'quote_length'
:
len
(
quote
),
}
start_index
+=
len
(
quote
)
while
True
:
quote_end_index
=
template
.
find
(
quote
,
start_index
)
backslash_index
=
template
.
find
(
"
\\
"
,
start_index
)
if
quote_end_index
<
0
:
return
result
if
0
<=
backslash_index
<
quote_end_index
:
start_index
=
backslash_index
+
2
else
:
result
[
'end_index'
]
=
quote_end_index
+
len
(
quote
)
return
result
class
UnderscoreTemplateLinter
(
object
):
...
...
@@ -1001,6 +1142,25 @@ class UnderscoreTemplateLinter(object):
return
expressions
def
_process_file
(
full_path
,
template_linters
,
options
,
out
):
"""
For each linter, lints the provided file. This means finding and printing
violations.
Arguments:
full_path: The full path of the file to lint.
template_linters: A list of linting objects.
options: A list of the options.
out: output file
"""
directory
=
os
.
path
.
dirname
(
full_path
)
file
=
os
.
path
.
basename
(
full_path
)
for
template_linter
in
template_linters
:
results
=
template_linter
.
process_file
(
directory
,
file
)
results
.
print_results
(
options
,
out
)
def
_process_current_walk
(
current_walk
,
template_linters
,
options
,
out
):
"""
For each linter, lints all the files in the current os walk. This means
...
...
@@ -1016,10 +1176,8 @@ def _process_current_walk(current_walk, template_linters, options, out):
walk_directory
=
os
.
path
.
normpath
(
current_walk
[
0
])
walk_files
=
current_walk
[
2
]
for
walk_file
in
walk_files
:
walk_file
=
os
.
path
.
normpath
(
walk_file
)
for
template_linter
in
template_linters
:
results
=
template_linter
.
process_file
(
walk_directory
,
walk_file
)
results
.
print_results
(
options
,
out
)
full_path
=
os
.
path
.
join
(
walk_directory
,
walk_file
)
_process_file
(
full_path
,
template_linters
,
options
,
out
)
def
_process_os_walk
(
starting_dir
,
template_linters
,
options
,
out
):
...
...
@@ -1037,33 +1195,60 @@ def _process_os_walk(starting_dir, template_linters, options, out):
_process_current_walk
(
current_walk
,
template_linters
,
options
,
out
)
def
_parse_arg
(
arg
,
option
):
"""
Parses an argument searching for --[option]=[OPTION_VALUE]
Arguments:
arg: The system argument
option: The specific option to be searched for (e.g. "file")
Returns:
The option value for a match, or None if arg is not for this option
"""
if
arg
.
startswith
(
'--{}='
.
format
(
option
)):
option_value
=
arg
.
split
(
'='
)[
1
]
if
option_value
.
startswith
(
"'"
)
or
option_value
.
startswith
(
'"'
):
option_value
=
option_value
[
1
:
-
1
]
return
option_value
else
:
return
None
def
main
():
"""
Used to execute the linter. Use --help option for help.
Prints all
of the
violations.
Prints all violations.
"""
epilog
=
'rules:
\n
'
for
rule
in
Rules
.
__members__
.
values
():
epilog
+=
" {0[0]}: {0[1]}
\n
"
.
format
(
rule
.
value
)
parser
=
argparse
.
ArgumentParser
(
formatter_class
=
argparse
.
RawDescriptionHelpFormatter
,
description
=
'Checks that templates are safe.'
,
epilog
=
epilog
)
parser
.
add_argument
(
'--quiet'
,
dest
=
'quiet'
,
action
=
'store_true'
,
help
=
'only display the filenames that contain violations'
)
parser
.
add_argument
(
'--file'
,
dest
=
'file'
,
nargs
=
1
,
default
=
None
,
help
=
'a single file to lint'
)
parser
.
add_argument
(
'--dir'
,
dest
=
'directory'
,
nargs
=
1
,
default
=
[
'.'
],
help
=
'the directory to lint (including sub-directories)'
)
#TODO: Use click
if
'--help'
in
sys
.
argv
:
print
(
"Check that templates are safe."
)
print
(
"Options:"
)
print
(
" --quiet Just display the filenames that have violations."
)
print
(
""
)
print
(
"Rules:"
)
for
rule
in
Rules
.
__members__
.
values
():
print
(
" {0[0]}: {0[1]}"
.
format
(
rule
.
value
))
return
is_quiet
=
'--quiet'
in
sys
.
argv
# TODO --file=...
args
=
parser
.
parse_args
()
options
=
{
'is_quiet'
:
is_
quiet
,
'is_quiet'
:
args
.
quiet
,
}
template_linters
=
[
MakoTemplateLinter
(),
UnderscoreTemplateLinter
()]
_process_os_walk
(
'.'
,
template_linters
,
options
,
out
=
sys
.
stdout
)
if
args
.
file
is
not
None
:
if
os
.
path
.
isfile
(
args
.
file
[
0
])
is
False
:
raise
ValueError
(
"File [{}] is not a valid file."
.
format
(
args
.
file
[
0
]))
_process_file
(
args
.
file
[
0
],
template_linters
,
options
,
out
=
sys
.
stdout
)
else
:
if
os
.
path
.
exists
(
args
.
directory
[
0
])
is
False
or
os
.
path
.
isfile
(
args
.
directory
[
0
])
is
True
:
raise
ValueError
(
"Directory [{}] is not a valid directory."
.
format
(
args
.
directory
[
0
]))
_process_os_walk
(
args
.
directory
[
0
],
template_linters
,
options
,
out
=
sys
.
stdout
)
if
__name__
==
"__main__"
:
...
...
scripts/tests/test_safe_template_linter.py
View file @
ff2e6dc1
...
...
@@ -109,7 +109,14 @@ class TestMakoTemplateLinter(TestCase):
if
data
[
'violations'
]
>
0
:
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
data
[
'rule'
])
def
test_check_mako_expressions_in_html
(
self
):
@data
(
{
'expression'
:
'${x}'
,
'rule'
:
None
},
{
'expression'
:
'${{unbalanced}'
,
'rule'
:
Rules
.
mako_unparseable_expression
},
{
'expression'
:
'${x | n}'
,
'rule'
:
Rules
.
mako_invalid_html_filter
},
{
'expression'
:
'${x | h}'
,
'rule'
:
Rules
.
mako_unwanted_html_filter
},
{
'expression'
:
'${x | n, dump_js_escaped_json}'
,
'rule'
:
Rules
.
mako_invalid_html_filter
},
)
def
test_check_mako_expressions_in_html
(
self
,
data
):
"""
Test _check_mako_file_is_safe in html context provides appropriate violations
"""
...
...
@@ -118,25 +125,12 @@ class TestMakoTemplateLinter(TestCase):
mako_template
=
textwrap
.
dedent
(
"""
<
%
page expression_filter="h"/>
${x}
${'{{unbalanced-nested'}
${x | n}
${x | h}
${x | n, dump_js_escaped_json}
"""
)
{expression}
"""
.
format
(
expression
=
data
[
'expression'
]))
linter
.
_check_mako_file_is_safe
(
mako_template
,
results
)
self
.
assertEqual
(
len
(
results
.
violations
),
4
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
Rules
.
mako_unparsable_expression
)
start_index
=
results
.
violations
[
0
]
.
expression
[
'start_index'
]
self
.
assertEqual
(
mako_template
[
start_index
:
start_index
+
24
],
"${'{{unbalanced-nested'}"
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
rule
,
Rules
.
mako_invalid_html_filter
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
expression
[
'expression'
],
"${x | n}"
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
rule
,
Rules
.
mako_unwanted_html_filter
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
expression
[
'expression'
],
"${x | h}"
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
rule
,
Rules
.
mako_invalid_html_filter
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
expression
[
'expression'
],
"${x | n, dump_js_escaped_json}"
)
self
.
_validate_data_rule
(
data
,
results
)
def
test_check_mako_expression_display_name
(
self
):
"""
...
...
@@ -156,6 +150,87 @@ class TestMakoTemplateLinter(TestCase):
self
.
assertEqual
(
len
(
results
.
violations
),
1
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
Rules
.
mako_deprecated_display_name
)
@data
(
{
'expression'
:
textwrap
.
dedent
(
"""
${"Mixed {span_start}text{span_end}".format(
span_start=HTML("<span>"),
span_end=HTML("</span>"),
)}
"""
),
'rule'
:
Rules
.
mako_html_requires_text
},
{
'expression'
:
textwrap
.
dedent
(
"""
${Text("Mixed {span_start}text{span_end}").format(
span_start=HTML("<span>"),
span_end=HTML("</span>"),
)}
"""
),
'rule'
:
None
},
{
'expression'
:
textwrap
.
dedent
(
"""
${"Mixed {span_start}{text}{span_end}".format(
span_start=HTML("<span>"),
text=Text("This should still break."),
span_end=HTML("</span>"),
)}
"""
),
'rule'
:
Rules
.
mako_html_requires_text
},
{
'expression'
:
textwrap
.
dedent
(
"""
${Text("Mixed {link_start}text{link_end}".format(
link_start=HTML("<a href='{}'>").format(url),
link_end=HTML("</a>"),
))}
"""
),
'rule'
:
Rules
.
mako_close_before_format
},
{
'expression'
:
textwrap
.
dedent
(
"""
${Text("Mixed {link_start}text{link_end}").format(
link_start=HTML("<a href='{}'>".format(url)),
link_end=HTML("</a>"),
)}
"""
),
'rule'
:
Rules
.
mako_close_before_format
},
{
'expression'
:
"""${ Text("text") }"""
,
'rule'
:
Rules
.
mako_text_redundant
},
{
'expression'
:
"""${ HTML("<span></span>") }"""
,
'rule'
:
None
},
{
'expression'
:
"""${ HTML("<span></span>") + "some other text" }"""
,
'rule'
:
Rules
.
mako_html_alone
},
)
def
test_check_mako_with_text_and_html
(
self
,
data
):
"""
Test _check_mako_file_is_safe tests for proper use of Text() and Html().
"""
linter
=
MakoTemplateLinter
()
results
=
FileResults
(
''
)
mako_template
=
textwrap
.
dedent
(
"""
<
%
page expression_filter="h"/>
{expression}
"""
.
format
(
expression
=
data
[
'expression'
]))
linter
.
_check_mako_file_is_safe
(
mako_template
,
results
)
self
.
_validate_data_rule
(
data
,
results
)
def
test_check_mako_expression_default_disabled
(
self
):
"""
Test _check_mako_file_is_safe with disable pragma for safe-by-default
...
...
@@ -228,7 +303,14 @@ class TestMakoTemplateLinter(TestCase):
self
.
assertEqual
(
len
(
results
.
violations
),
1
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
Rules
.
mako_missing_default
)
def
test_check_mako_expressions_in_javascript
(
self
):
@data
(
{
'expression'
:
'${x}'
,
'rule'
:
Rules
.
mako_invalid_js_filter
},
{
'expression'
:
'${{unbalanced}'
,
'rule'
:
Rules
.
mako_unparseable_expression
},
{
'expression'
:
'${x | n}'
,
'rule'
:
Rules
.
mako_invalid_js_filter
},
{
'expression'
:
'${x | h}'
,
'rule'
:
Rules
.
mako_invalid_js_filter
},
{
'expression'
:
'${x | n, dump_js_escaped_json}'
,
'rule'
:
None
},
)
def
test_check_mako_expressions_in_javascript
(
self
,
data
):
"""
Test _check_mako_file_is_safe in JavaScript script context provides
appropriate violations
...
...
@@ -239,29 +321,19 @@ class TestMakoTemplateLinter(TestCase):
mako_template
=
textwrap
.
dedent
(
"""
<
%
page expression_filter="h"/>
<script>
${x}
${'{{unbalanced-nested'}
${x | n}
${x | h}
${x | n, dump_js_escaped_json}
"${x-with-quotes | n, js_escaped_string}"
{expression}
</script>
"""
)
"""
.
format
(
expression
=
data
[
'expression'
])
)
linter
.
_check_mako_file_is_safe
(
mako_template
,
results
)
self
.
assertEqual
(
len
(
results
.
violations
),
4
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
Rules
.
mako_invalid_js_filter
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
expression
[
'expression'
],
"${x}"
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
rule
,
Rules
.
mako_unparsable_expression
)
start_index
=
results
.
violations
[
1
]
.
expression
[
'start_index'
]
self
.
assertEqual
(
mako_template
[
start_index
:
start_index
+
24
],
"${'{{unbalanced-nested'}"
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
rule
,
Rules
.
mako_invalid_js_filter
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
expression
[
'expression'
],
"${x | n}"
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
rule
,
Rules
.
mako_invalid_js_filter
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
expression
[
'expression'
],
"${x | h}"
)
self
.
_validate_data_rule
(
data
,
results
)
def
test_check_mako_expressions_in_require_js
(
self
):
@data
(
{
'expression'
:
'${x}'
,
'rule'
:
Rules
.
mako_invalid_js_filter
},
{
'expression'
:
'${x | n, js_escaped_string}'
,
'rule'
:
None
},
)
def
test_check_mako_expressions_in_require_js
(
self
,
data
):
"""
Test _check_mako_file_is_safe in JavaScript require context provides
appropriate violations
...
...
@@ -271,17 +343,14 @@ class TestMakoTemplateLinter(TestCase):
mako_template
=
textwrap
.
dedent
(
"""
<
%
page expression_filter="h"/>
<
%
static:require_module module_name="${x}" class_name="TestFactory">
${x}
${x | n, js_escaped_string}
<
%
static:require_module module_name="${{x}}" class_name="TestFactory">
{expression}
</
%
static:require_module>
"""
)
"""
.
format
(
expression
=
data
[
'expression'
])
)
linter
.
_check_mako_file_is_safe
(
mako_template
,
results
)
self
.
assertEqual
(
len
(
results
.
violations
),
1
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
Rules
.
mako_invalid_js_filter
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
expression
[
'expression'
],
"${x}"
)
self
.
_validate_data_rule
(
data
,
results
)
@data
(
{
'media_type'
:
'text/javascript'
,
'expected_violations'
:
0
},
...
...
@@ -339,7 +408,20 @@ class TestMakoTemplateLinter(TestCase):
self
.
assertEqual
(
results
.
violations
[
3
]
.
rule
,
Rules
.
mako_invalid_js_filter
)
self
.
assertEqual
(
results
.
violations
[
4
]
.
rule
,
Rules
.
mako_unwanted_html_filter
)
def
test_expression_detailed_results
(
self
):
@data
(
{
'template'
:
"
\n
${x | n}"
,
'parseable'
:
True
},
{
'template'
:
textwrap
.
dedent
(
"""
<div>${(
'tabbed-multi-line-expression'
) | n}</div>
"""
),
'parseable'
:
True
},
{
'template'
:
"${{unparseable}"
,
'parseable'
:
False
},
)
def
test_expression_detailed_results
(
self
,
data
):
"""
Test _check_mako_file_is_safe provides detailed results, including line
numbers, columns, and line
...
...
@@ -347,86 +429,115 @@ class TestMakoTemplateLinter(TestCase):
linter
=
MakoTemplateLinter
()
results
=
FileResults
(
''
)
mako_template
=
textwrap
.
dedent
(
"""
${x | n}
<div>${(
'tabbed-multi-line-expression'
) | n}</div>
${'{{unbalanced-nested' | n}
"""
)
linter
.
_check_mako_file_is_safe
(
mako_template
,
results
)
linter
.
_check_mako_file_is_safe
(
data
[
'template'
],
results
)
self
.
assertEqual
(
len
(
results
.
violations
),
4
)
self
.
assertEqual
(
len
(
results
.
violations
),
2
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
Rules
.
mako_missing_default
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
start_line
,
2
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
start_column
,
1
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
end_line
,
2
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
end_column
,
8
)
self
.
assertEqual
(
len
(
results
.
violations
[
1
]
.
lines
),
1
)
self
.
assertEqual
(
results
.
violations
[
1
]
.
lines
[
0
],
"${x | n}"
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
start_line
,
3
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
start_column
,
10
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
end_line
,
5
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
end_column
,
10
)
self
.
assertEqual
(
len
(
results
.
violations
[
2
]
.
lines
),
3
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
lines
[
0
],
" <div>${("
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
lines
[
1
],
" 'tabbed-multi-line-expression'"
)
self
.
assertEqual
(
results
.
violations
[
2
]
.
lines
[
2
],
" ) | n}</div>"
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
start_line
,
6
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
start_column
,
1
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
end_line
,
6
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
end_column
,
"?"
)
self
.
assertEqual
(
len
(
results
.
violations
[
3
]
.
lines
),
1
)
self
.
assertEqual
(
results
.
violations
[
3
]
.
lines
[
0
],
"${'{{unbalanced-nested' | n}"
)
def
test_find_mako_expressions
(
self
):
"""
Test _find_mako_expressions finds appropriate expressions
violation
=
results
.
violations
[
1
]
lines
=
list
(
data
[
'template'
]
.
splitlines
())
self
.
assertTrue
(
"${"
in
lines
[
violation
.
start_line
-
1
])
self
.
assertTrue
(
lines
[
violation
.
start_line
-
1
]
.
startswith
(
"${"
,
violation
.
start_column
-
1
))
if
data
[
'parseable'
]:
self
.
assertTrue
(
"}"
in
lines
[
violation
.
end_line
-
1
])
self
.
assertTrue
(
lines
[
violation
.
end_line
-
1
]
.
startswith
(
"}"
,
violation
.
end_column
-
1
))
else
:
self
.
assertEqual
(
violation
.
start_line
,
violation
.
end_line
)
self
.
assertEqual
(
violation
.
end_column
,
"?"
)
self
.
assertEqual
(
len
(
violation
.
lines
),
violation
.
end_line
-
violation
.
start_line
+
1
)
for
line_index
in
range
(
0
,
len
(
violation
.
lines
)):
self
.
assertEqual
(
violation
.
lines
[
line_index
],
lines
[
line_index
+
violation
.
start_line
-
1
])
@data
(
{
'template'
:
"${x}"
},
{
'template'
:
"
\n
${x}"
},
{
'template'
:
"${x} "
},
{
'template'
:
"${{test-balanced-delims}} "
},
{
'template'
:
"${'{unbalanced in string'}"
},
{
'template'
:
"${'unbalanced in string}'}"
},
{
'template'
:
"${(
\n
'tabbed-multi-line-expression'
\n
)}"
},
)
def
test_find_mako_expressions
(
self
,
data
):
"""
Test _find_mako_expressions for parseable expressions
"""
linter
=
MakoTemplateLinter
()
mako_template
=
textwrap
.
dedent
(
"""
${x}
${tabbed-x}
${(
'tabbed-multi-line-expression'
)}
${'{{unbalanced-nested'}
${'{{nested}}'}
<div>no expression</div>
"""
)
expressions
=
linter
.
_find_mako_expressions
(
data
[
'template'
])
expressions
=
linter
.
_find_mako_expressions
(
mako_template
)
self
.
assertEqual
(
len
(
expressions
),
1
)
start_index
=
expressions
[
0
][
'start_index'
]
end_index
=
expressions
[
0
][
'end_index'
]
self
.
assertEqual
(
data
[
'template'
][
start_index
:
end_index
+
1
],
data
[
'template'
]
.
strip
())
self
.
assertEqual
(
expressions
[
0
][
'expression'
],
data
[
'template'
]
.
strip
())
self
.
assertEqual
(
len
(
expressions
),
5
)
self
.
_validate_expression
(
mako_template
,
expressions
[
0
],
'${x}'
)
self
.
_validate_expression
(
mako_template
,
expressions
[
1
],
'${tabbed-x}'
)
self
.
_validate_expression
(
mako_template
,
expressions
[
2
],
"${(
\n
'tabbed-multi-line-expression'
\n
)}"
)
@data
(
{
'template'
:
" ${{unparseable} ${}"
,
'start_index'
:
1
},
{
'template'
:
" ${'unparseable} ${}"
,
'start_index'
:
1
},
)
def
test_find_mako_expressions
(
self
,
data
):
"""
Test _find_mako_expressions for unparseable expressions
"""
linter
=
MakoTemplateLinter
()
# won't parse unbalanced nested {}'s
unbalanced_expression
=
"${'{{unbalanced-nested'}"
self
.
assertEqual
(
expressions
[
3
][
'end_index'
],
-
1
)
start_index
=
expressions
[
3
][
'start_index'
]
self
.
assertEqual
(
mako_template
[
start_index
:
start_index
+
len
(
unbalanced_expression
)],
unbalanced_expression
)
self
.
assertEqual
(
expressions
[
3
][
'expression'
],
None
)
expressions
=
linter
.
_find_mako_expressions
(
data
[
'template'
])
self
.
assertTrue
(
2
<=
len
(
expressions
))
self
.
assertEqual
(
expressions
[
0
][
'start_index'
],
data
[
'start_index'
])
self
.
assertIsNone
(
expressions
[
0
][
'expression'
])
self
.
_validate_expression
(
mako_template
,
expressions
[
4
],
"${'{{nested}}'}"
)
@data
(
{
'template'
:
"""${""}"""
,
'start_index'
:
0
,
'end_index'
:
5
,
'expected_index'
:
2
},
{
'template'
:
"""${''}"""
,
'start_index'
:
0
,
'end_index'
:
5
,
'expected_index'
:
2
},
{
'template'
:
"""${"''"}"""
,
'start_index'
:
0
,
'end_index'
:
7
,
'expected_index'
:
2
},
{
'template'
:
"""${'""'}"""
,
'start_index'
:
0
,
'end_index'
:
7
,
'expected_index'
:
2
},
{
'template'
:
"""${'""'}"""
,
'start_index'
:
3
,
'end_index'
:
7
,
'expected_index'
:
3
},
{
'template'
:
"""${'""'}"""
,
'start_index'
:
0
,
'end_index'
:
1
,
'expected_index'
:
-
1
},
)
def
test_find_string_start
(
self
,
data
):
"""
Test _find_string_start helper
"""
linter
=
MakoTemplateLinter
()
string_start_index
=
linter
.
_find_string_start
(
data
[
'template'
],
data
[
'start_index'
],
data
[
'end_index'
])
self
.
assertEqual
(
string_start_index
,
data
[
'expected_index'
])
@data
(
{
'template'
:
'${""}'
,
'result'
:
{
'start_index'
:
2
,
'end_index'
:
4
,
'quote_length'
:
1
}
},
{
'template'
:
"${'Hello'}"
,
'result'
:
{
'start_index'
:
2
,
'end_index'
:
9
,
'quote_length'
:
1
}
},
{
'template'
:
'${""" triple """}'
,
'result'
:
{
'start_index'
:
2
,
'end_index'
:
16
,
'quote_length'
:
3
}
},
{
'template'
:
r""" ${" \" \\"} """
,
'result'
:
{
'start_index'
:
3
,
'end_index'
:
11
,
'quote_length'
:
1
}
},
)
def
test_parse_string
(
self
,
data
):
"""
Test _parse_string helper
"""
linter
=
MakoTemplateLinter
()
result
=
linter
.
_parse_string
(
data
[
'template'
],
data
[
'result'
][
'start_index'
])
def
_validate_expression
(
self
,
template_string
,
expression
,
expected_expression
):
start_index
=
expression
[
'start_index'
]
end_index
=
expression
[
'end_index'
]
self
.
assertEqual
(
template_string
[
start_index
:
end_index
+
1
],
expected_expression
)
self
.
assertEqual
(
expression
[
'expression'
],
expected_expression
)
self
.
assertDictEqual
(
result
,
data
[
'result'
])
def
_validate_data_rule
(
self
,
data
,
results
):
if
data
[
'rule'
]
is
None
:
self
.
assertEqual
(
len
(
results
.
violations
),
0
)
else
:
self
.
assertEqual
(
len
(
results
.
violations
),
1
)
self
.
assertEqual
(
results
.
violations
[
0
]
.
rule
,
data
[
'rule'
])
@ddt
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment