Commit d1bda204 by Robert Raposa

Enhance Mako linting

- Lint JavaScript context for JavaScript violations
- Lint for Mako specific JavaScript rules
- Skip commented lines
- Change unicode to decode.utf8
- Count lint violations
parent 11982461
...@@ -40,7 +40,7 @@ def _is_skip_dir(skip_dirs, directory): ...@@ -40,7 +40,7 @@ def _is_skip_dir(skip_dirs, directory):
return False return False
def _load_file(self, file_full_path): def _load_file(file_full_path):
""" """
Loads a file into a string. Loads a file into a string.
...@@ -74,32 +74,30 @@ def _find_closing_char_index(start_delim, open_char, close_char, template, start ...@@ -74,32 +74,30 @@ def _find_closing_char_index(start_delim, open_char, close_char, template, start
strings: A list of ParseStrings already parsed strings: A list of ParseStrings already parsed
Returns: Returns:
A dict containing the following: A dict containing the following, or None if unparseable:
close_char_index: The index of the closing character, or -1 if close_char_index: The index of the closing character
unparseable.
strings: a list of ParseStrings strings: a list of ParseStrings
""" """
strings = [] if strings is None else strings strings = [] if strings is None else strings
unparseable_result = {'close_char_index': -1, 'strings': []}
close_char_index = template.find(close_char, start_index) close_char_index = template.find(close_char, start_index)
if close_char_index < 0: if close_char_index < 0:
# if we can't find an end_char, let's just quit # if we can't find an end_char, let's just quit
return unparseable_result return None
open_char_index = template.find(open_char, start_index, close_char_index) open_char_index = template.find(open_char, start_index, close_char_index)
parse_string = ParseString(template, start_index, close_char_index) parse_string = ParseString(template, start_index, close_char_index)
valid_index_list = [close_char_index] valid_index_list = [close_char_index]
if 0 <= open_char_index: if 0 <= open_char_index:
valid_index_list.append(open_char_index) valid_index_list.append(open_char_index)
if 0 <= parse_string.start_index: if parse_string.start_index is not None:
valid_index_list.append(parse_string.start_index) valid_index_list.append(parse_string.start_index)
min_valid_index = min(valid_index_list) min_valid_index = min(valid_index_list)
if parse_string.start_index == min_valid_index: if parse_string.start_index == min_valid_index:
strings.append(parse_string) strings.append(parse_string)
if parse_string.end_index < 0: if parse_string.end_index is None:
return unparseable_result return None
else: else:
return _find_closing_char_index( return _find_closing_char_index(
start_delim, open_char, close_char, template, start_index=parse_string.end_index, start_delim, open_char, close_char, template, start_index=parse_string.end_index,
...@@ -111,7 +109,7 @@ def _find_closing_char_index(start_delim, open_char, close_char, template, start ...@@ -111,7 +109,7 @@ def _find_closing_char_index(start_delim, open_char, close_char, template, start
# if we find another starting delim, consider this unparseable # if we find another starting delim, consider this unparseable
start_delim_index = template.find(start_delim, start_index, close_char_index) start_delim_index = template.find(start_delim, start_index, close_char_index)
if 0 <= start_delim_index < open_char_index: if 0 <= start_delim_index < open_char_index:
return unparseable_result return None
return _find_closing_char_index( return _find_closing_char_index(
start_delim, open_char, close_char, template, start_index=open_char_index + 1, start_delim, open_char, close_char, template, start_index=open_char_index + 1,
num_open_chars=num_open_chars + 1, strings=strings num_open_chars=num_open_chars + 1, strings=strings
...@@ -145,7 +143,10 @@ class StringLines(object): ...@@ -145,7 +143,10 @@ class StringLines(object):
""" """
self._string = string self._string = string
self._line_breaks = self._process_line_breaks(string) self._line_start_indexes = self._process_line_breaks(string)
# this is an exclusive index used in the case that the template doesn't
# end with a new line
self.eof_index = len(string)
def _process_line_breaks(self, string): def _process_line_breaks(self, string):
""" """
...@@ -156,19 +157,18 @@ class StringLines(object): ...@@ -156,19 +157,18 @@ class StringLines(object):
string: The string in which to find line breaks. string: The string in which to find line breaks.
Returns: Returns:
A list of indices into the string at which each line break can be A list of indices into the string at which each line begins.
found.
""" """
line_breaks = [0] line_start_indexes = [0]
index = 0 index = 0
while True: while True:
index = string.find('\n', index) index = string.find('\n', index)
if index < 0: if index < 0:
break break
index += 1 index += 1
line_breaks.append(index) line_start_indexes.append(index)
return line_breaks return line_start_indexes
def get_string(self): def get_string(self):
""" """
...@@ -189,7 +189,7 @@ class StringLines(object): ...@@ -189,7 +189,7 @@ class StringLines(object):
""" """
current_line_number = 0 current_line_number = 0
for line_break_index in self._line_breaks: for line_break_index in self._line_start_indexes:
if line_break_index <= index: if line_break_index <= index:
current_line_number += 1 current_line_number += 1
else: else:
...@@ -227,6 +227,20 @@ class StringLines(object): ...@@ -227,6 +227,20 @@ class StringLines(object):
line_number = self.index_to_line_number(index) line_number = self.index_to_line_number(index)
return self.line_number_to_start_index(line_number) return self.line_number_to_start_index(line_number)
def index_to_line_end_index(self, index):
"""
Gets the index of the end of the line of the given index.
Arguments:
index: The index into the original string.
Returns:
The index of the end of the line of the given index.
"""
line_number = self.index_to_line_number(index)
return self.line_number_to_end_index(line_number)
def line_number_to_start_index(self, line_number): def line_number_to_start_index(self, line_number):
""" """
Gets the starting index for the provided line number. Gets the starting index for the provided line number.
...@@ -239,7 +253,26 @@ class StringLines(object): ...@@ -239,7 +253,26 @@ class StringLines(object):
The starting index for the provided line number. The starting index for the provided line number.
""" """
return self._line_breaks[line_number - 1] return self._line_start_indexes[line_number - 1]
def line_number_to_end_index(self, line_number):
"""
Gets the ending index for the provided line number.
Arguments:
line_number: The line number of the line for which we want to find
the end index.
Returns:
The ending index for the provided line number.
"""
if line_number < len(self._line_start_indexes):
return self._line_start_indexes[line_number]
else:
# an exclusive index in the case that the file didn't end with a
# newline.
return self.eof_index
def line_number_to_line(self, line_number): def line_number_to_line(self, line_number):
""" """
...@@ -252,11 +285,11 @@ class StringLines(object): ...@@ -252,11 +285,11 @@ class StringLines(object):
The line of text designated by the provided line number. The line of text designated by the provided line number.
""" """
start_index = self._line_breaks[line_number - 1] start_index = self._line_start_indexes[line_number - 1]
if len(self._line_breaks) == line_number: if len(self._line_start_indexes) == line_number:
line = self._string[start_index:] line = self._string[start_index:]
else: else:
end_index = self._line_breaks[line_number] end_index = self._line_start_indexes[line_number]
line = self._string[start_index:end_index - 1] line = self._string[start_index:end_index - 1]
return line return line
...@@ -264,7 +297,7 @@ class StringLines(object): ...@@ -264,7 +297,7 @@ class StringLines(object):
""" """
Gets the number of lines in the string. Gets the number of lines in the string.
""" """
return len(self._line_breaks) return len(self._line_start_indexes)
class Rules(Enum): class Rules(Enum):
...@@ -295,6 +328,14 @@ class Rules(Enum): ...@@ -295,6 +328,14 @@ class Rules(Enum):
'mako-invalid-js-filter', 'mako-invalid-js-filter',
'The expression is using an invalid filter in a JavaScript context.' 'The expression is using an invalid filter in a JavaScript context.'
) )
mako_js_missing_quotes = (
'mako-js-missing-quotes',
'An expression using js_escaped_string must be wrapped in quotes.'
)
mako_js_html_string = (
'mako-js-html-string',
'A JavaScript string containing HTML should not have an embedded Mako expression.'
)
mako_deprecated_display_name = ( mako_deprecated_display_name = (
'mako-deprecated-display-name', 'mako-deprecated-display-name',
'Replace deprecated display_name_with_default_escaped with display_name_with_default.' 'Replace deprecated display_name_with_default_escaped with display_name_with_default.'
...@@ -365,6 +406,57 @@ class Rules(Enum): ...@@ -365,6 +406,57 @@ class Rules(Enum):
self.rule_summary = rule_summary self.rule_summary = rule_summary
class Expression(object):
"""
Represents an arbitrary expression.
An expression can be any type of code snippet. It will sometimes have a
starting and ending delimiter, but not always.
Here are some example expressions::
${x | n, decode.utf8}
<%= x %>
function(x)
"<p>" + message + "</p>"
Other details of note:
- Only a start_index is required for a valid expression.
- If end_index is None, it means we couldn't parse the rest of the
expression.
- All other details of the expression are optional, and are only added if
and when supplied and needed for additional checks. They are not necessary
for the final results output.
"""
def __init__(self, start_index, end_index=None, template=None, start_delim="", end_delim="", strings=None):
"""
Init method.
Arguments:
start_index: the starting index of the expression
end_index: the index immediately following the expression, or None
if the expression was unparseable
template: optional template code in which the expression was found
start_delim: optional starting delimiter of the expression
end_delim: optional ending delimeter of the expression
strings: optional list of ParseStrings
"""
self.start_index = start_index
self.end_index = end_index
self.start_delim = start_delim
self.end_delim = end_delim
self.strings = strings
if template is not None and self.end_index is not None:
self.expression = template[start_index:end_index]
self.expression_inner = self.expression[len(start_delim):-len(end_delim)].strip()
else:
self.expression = None
self.expression_inner = None
class RuleViolation(object): class RuleViolation(object):
""" """
Base class representing a rule violation which can be used for reporting. Base class representing a rule violation which can be used for reporting.
...@@ -423,6 +515,12 @@ class RuleViolation(object): ...@@ -423,6 +515,12 @@ class RuleViolation(object):
""" """
return 0 return 0
def first_line(self):
"""
Since a file level rule has no first line, returns empty string.
"""
return ''
def prepare_results(self, full_path, string_lines): def prepare_results(self, full_path, string_lines):
""" """
Preps this instance for results reporting. Preps this instance for results reporting.
...@@ -460,7 +558,7 @@ class ExpressionRuleViolation(RuleViolation): ...@@ -460,7 +558,7 @@ class ExpressionRuleViolation(RuleViolation):
Arguments: Arguments:
rule: The Rule which was violated. rule: The Rule which was violated.
expression: The expression that was in violation. expression: The Expression that was in violation.
""" """
super(ExpressionRuleViolation, self).__init__(rule) super(ExpressionRuleViolation, self).__init__(rule)
...@@ -517,6 +615,12 @@ class ExpressionRuleViolation(RuleViolation): ...@@ -517,6 +615,12 @@ class ExpressionRuleViolation(RuleViolation):
""" """
return (self.start_line, self.start_column) return (self.start_line, self.start_column)
def first_line(self):
"""
Returns the initial line of code of the violation.
"""
return self.lines[0]
def prepare_results(self, full_path, string_lines): def prepare_results(self, full_path, string_lines):
""" """
Preps this instance for results reporting. Preps this instance for results reporting.
...@@ -528,11 +632,11 @@ class ExpressionRuleViolation(RuleViolation): ...@@ -528,11 +632,11 @@ class ExpressionRuleViolation(RuleViolation):
""" """
self.full_path = full_path self.full_path = full_path
start_index = self.expression['start_index'] start_index = self.expression.start_index
self.start_line = string_lines.index_to_line_number(start_index) self.start_line = string_lines.index_to_line_number(start_index)
self.start_column = string_lines.index_to_column_number(start_index) self.start_column = string_lines.index_to_column_number(start_index)
end_index = self.expression['end_index'] end_index = self.expression.end_index
if end_index > 0: if end_index is not None:
self.end_line = string_lines.index_to_line_number(end_index) self.end_line = string_lines.index_to_line_number(end_index)
self.end_column = string_lines.index_to_column_number(end_index) self.end_column = string_lines.index_to_column_number(end_index)
else: else:
...@@ -584,17 +688,21 @@ class FileResults(object): ...@@ -584,17 +688,21 @@ class FileResults(object):
self.is_file = os.path.isfile(full_path) self.is_file = os.path.isfile(full_path)
self.violations = [] self.violations = []
def prepare_results(self, file_string): def prepare_results(self, file_string, line_comment_delim=None):
""" """
Prepares the results for output for this file. Prepares the results for output for this file.
Arguments: Arguments:
file_string: The string of content for this file. file_string: The string of content for this file.
line_comment_delim: A string representing the start of a line
comment. For example "##" for Mako and "//" for JavaScript.
""" """
string_lines = StringLines(file_string) string_lines = StringLines(file_string)
for violation in self.violations: for violation in self.violations:
violation.prepare_results(self.full_path, string_lines) violation.prepare_results(self.full_path, string_lines)
if line_comment_delim is not None:
self._filter_commented_code(line_comment_delim)
def print_results(self, options, out): def print_results(self, options, out):
""" """
...@@ -606,16 +714,49 @@ class FileResults(object): ...@@ -606,16 +714,49 @@ class FileResults(object):
all violations. all violations.
out: output file out: output file
Returns:
The number of violations. When using --quiet, returns number of
files with violations.
""" """
num_violations = 0
if options['is_quiet']: if options['is_quiet']:
if self.violations is not None and 0 < len(self.violations): if self.violations is not None and 0 < len(self.violations):
num_violations += 1
print(self.full_path, file=out) print(self.full_path, file=out)
else: else:
self.violations.sort(key=lambda violation: violation.sort_key()) self.violations.sort(key=lambda violation: violation.sort_key())
for violation in self.violations: for violation in self.violations:
if not violation.is_disabled: if not violation.is_disabled:
num_violations += 1
violation.print_results(out) violation.print_results(out)
return num_violations
def _filter_commented_code(self, line_comment_delim):
"""
Remove any violations that were found in commented out code.
Arguments:
line_comment_delim: A string representing the start of a line
comment. For example "##" for Mako and "//" for JavaScript.
"""
self.violations = [v for v in self.violations if not self._is_commented(v, line_comment_delim)]
def _is_commented(self, violation, line_comment_delim):
"""
Checks if violation line is commented out.
Arguments:
violation: The violation to check
line_comment_delim: A string representing the start of a line
comment. For example "##" for Mako and "//" for JavaScript.
Returns:
True if the first line of the violation is actually commented out,
False otherwise.
"""
return violation.first_line().lstrip().startswith(line_comment_delim)
class ParseString(object): class ParseString(object):
...@@ -623,8 +764,8 @@ class ParseString(object): ...@@ -623,8 +764,8 @@ class ParseString(object):
ParseString is the result of parsing a string out of a template. ParseString is the result of parsing a string out of a template.
A ParseString has the following attributes: A ParseString has the following attributes:
start_index: The index of the first quote, or -1 if none found start_index: The index of the first quote, or None if none found
end_index: The index following the closing quote, or -1 if end_index: The index following the closing quote, or None if
unparseable unparseable
quote_length: The length of the quote. Could be 3 for a Python quote_length: The length of the quote. Could be 3 for a Python
triple quote. Or None if none found. triple quote. Or None if none found.
...@@ -644,12 +785,12 @@ class ParseString(object): ...@@ -644,12 +785,12 @@ class ParseString(object):
end_index: The end index to search before. end_index: The end index to search before.
""" """
self.end_index = -1 self.end_index = None
self.quote_length = None self.quote_length = None
self.string = None self.string = None
self.string_inner = None self.string_inner = None
self.start_index = self._find_string_start(template, start_index, end_index) self.start_index = self._find_string_start(template, start_index, end_index)
if 0 <= self.start_index: if self.start_index is not None:
result = self._parse_string(template, self.start_index) result = self._parse_string(template, self.start_index)
if result is not None: if result is not None:
self.end_index = result['end_index'] self.end_index = result['end_index']
...@@ -668,13 +809,13 @@ class ParseString(object): ...@@ -668,13 +809,13 @@ class ParseString(object):
end_index: The end index to search before. end_index: The end index to search before.
Returns: Returns:
The start index of the first single or double quote, or -1 if The start index of the first single or double quote, or None if no
no quote was found. quote was found.
""" """
quote_regex = re.compile(r"""['"]""") quote_regex = re.compile(r"""['"]""")
start_match = quote_regex.search(template, start_index, end_index) start_match = quote_regex.search(template, start_index, end_index)
if start_match is None: if start_match is None:
return -1 return None
else: else:
return start_match.start() return start_match.start()
...@@ -722,970 +863,1096 @@ class ParseString(object): ...@@ -722,970 +863,1096 @@ class ParseString(object):
} }
class MakoTemplateLinter(object): class UnderscoreTemplateLinter(object):
""" """
The linter for Mako template files. The linter for Underscore.js template files.
""" """
_skip_mako_dirs = _skip_dirs _skip_underscore_dirs = _skip_dirs + ('test',)
def process_file(self, directory, file_name): def process_file(self, directory, file_name):
""" """
Process file to determine if it is a Mako template file and Process file to determine if it is an Underscore template file and
if it is safe. if it is safe.
Arguments: Arguments:
directory (string): The directory of the file to be checked directory (string): The directory of the file to be checked
file_name (string): A filename for a potential Mako file file_name (string): A filename for a potential underscore file
Returns: Returns:
The file results containing any violations. The file results containing any violations.
""" """
mako_file_full_path = os.path.normpath(directory + '/' + file_name) full_path = os.path.normpath(directory + '/' + file_name)
results = FileResults(mako_file_full_path) results = FileResults(full_path)
if not results.is_file:
return results
if not self._is_valid_directory(directory): if not self._is_valid_directory(directory):
return results return results
# TODO: When safe-by-default is turned on at the platform level, will we: if not file_name.lower().endswith('.underscore'):
# 1. Turn it on for .html only, or
# 2. Turn it on for all files, and have different rulesets that have
# different rules of .xml, .html, .js, .txt Mako templates (e.g. use
# the n filter to turn off h for some of these)?
# For now, we only check .html and .xml files
if not (file_name.lower().endswith('.html') or file_name.lower().endswith('.xml')):
return results return results
return self._load_and_check_mako_file_is_safe(mako_file_full_path, results) return self._load_and_check_underscore_file_is_safe(full_path, results)
def _is_valid_directory(self, directory): def _is_valid_directory(self, directory):
""" """
Determines if the provided directory is a directory that could contain Determines if the provided directory is a directory that could contain
Mako template files that need to be linted. Underscore.js template files that need to be linted.
Arguments: Arguments:
directory: The directory to be linted. directory: The directory to be linted.
Returns: Returns:
True if this directory should be linted for Mako template violations True if this directory should be linted for Underscore.js template
and False otherwise. violations and False otherwise.
""" """
if _is_skip_dir(self._skip_mako_dirs, directory): if _is_skip_dir(self._skip_underscore_dirs, directory):
return False return False
# TODO: This is an imperfect guess concerning the Mako template return True
# directories. This needs to be reviewed before turning on safe by
# default at the platform level.
if ('/templates/' in directory) or directory.endswith('/templates'):
return True
return False
def _load_and_check_mako_file_is_safe(self, mako_file_full_path, results): def _load_and_check_underscore_file_is_safe(self, file_full_path, results):
""" """
Loads the Mako template file and checks if it is in violation. Loads the Underscore.js template file and checks if it is in violation.
Arguments: Arguments:
mako_file_full_path: The file to be loaded and linted. file_full_path: The file to be loaded and linted
Returns: Returns:
The file results containing any violations. The file results containing any violations.
""" """
mako_template = _load_file(self, mako_file_full_path) underscore_template = _load_file(file_full_path)
self._check_mako_file_is_safe(mako_template, results) self.check_underscore_file_is_safe(underscore_template, results)
return results return results
def _check_mako_file_is_safe(self, mako_template, results): def check_underscore_file_is_safe(self, underscore_template, results):
""" """
Checks for violations in a Mako template. Checks for violations in an Underscore.js template.
Arguments: Arguments:
mako_template: The contents of the Mako template. underscore_template: The contents of the Underscore.js template.
results: A file results objects to which violations will be added. results: A file results objects to which violations will be added.
""" """
if self._is_django_template(mako_template): self._check_underscore_expressions(underscore_template, results)
return results.prepare_results(underscore_template)
has_page_default = False
if self._has_multiple_page_tags(mako_template):
results.violations.append(RuleViolation(Rules.mako_multiple_page_tags))
else:
has_page_default = self._has_page_default(mako_template)
if not has_page_default:
results.violations.append(RuleViolation(Rules.mako_missing_default))
self._check_mako_expressions(mako_template, has_page_default, results)
results.prepare_results(mako_template)
def _is_django_template(self, mako_template): def _check_underscore_expressions(self, underscore_template, results):
""" """
Determines if the template is actually a Django template. Searches for Underscore.js expressions that contain violations.
Arguments: Arguments:
mako_template: The template code. underscore_template: The contents of the Underscore.js template.
results: A list of results into which violations will be added.
"""
expressions = self._find_unescaped_expressions(underscore_template)
for expression in expressions:
if not self._is_safe_unescaped_expression(expression):
results.violations.append(ExpressionRuleViolation(
Rules.underscore_not_escaped, expression
))
def _is_safe_unescaped_expression(self, expression):
"""
Determines whether an expression is safely escaped, even though it is
using the expression syntax that doesn't itself escape (i.e. <%= ).
In some cases it is ok to not use the Underscore.js template escape
(i.e. <%- ) because the escaping is happening inside the expression.
Safe examples::
<%= HtmlUtils.ensureHtml(message) %>
<%= _.escape(message) %>
Arguments:
expression: The Expression being checked.
Returns: Returns:
True if this is really a Django template, and False otherwise. True if the Expression has been safely escaped, and False otherwise.
""" """
if re.search('({%.*%})|({{.*}})', mako_template) is not None: if expression.expression_inner.startswith('HtmlUtils.'):
return True
if expression.expression_inner.startswith('_.escape('):
return True return True
return False return False
def _has_multiple_page_tags(self, mako_template): def _find_unescaped_expressions(self, underscore_template):
""" """
Checks if the Mako template contains more than one page expression. Returns a list of unsafe expressions.
At this time all expressions that are unescaped are considered unsafe.
Arguments: Arguments:
mako_template: The contents of the Mako template. underscore_template: The contents of the Underscore.js template.
Returns:
A list of Expressions.
""" """
count = len(re.findall('<%page ', mako_template, re.IGNORECASE)) unescaped_expression_regex = re.compile("<%=.*?%>", re.DOTALL)
return count > 1
expressions = []
for match in unescaped_expression_regex.finditer(underscore_template):
expression = Expression(
match.start(), match.end(), template=underscore_template, start_delim="<%=", end_delim="%>"
)
expressions.append(expression)
return expressions
class JavaScriptLinter(object):
"""
The linter for JavaScript and CoffeeScript files.
"""
def _has_page_default(self, mako_template): _skip_javascript_dirs = _skip_dirs + ('i18n', 'static/coffee')
_skip_coffeescript_dirs = _skip_dirs
underScoreLinter = UnderscoreTemplateLinter()
def process_file(self, directory, file_name):
""" """
Checks if the Mako template contains the page expression marking it as Process file to determine if it is a JavaScript file and
safe by default. if it is safe.
Arguments: Arguments:
mako_template: The contents of the Mako template. directory (string): The directory of the file to be checked
file_name (string): A filename for a potential JavaScript file
Returns:
The file results containing any violations.
""" """
page_h_filter_regex = re.compile('<%page[^>]*expression_filter=(?:"h"|\'h\')[^>]*/>') file_full_path = os.path.normpath(directory + '/' + file_name)
page_match = page_h_filter_regex.search(mako_template) results = FileResults(file_full_path)
return page_match
def _check_mako_expressions(self, mako_template, has_page_default, results): if not results.is_file:
return results
if file_name.lower().endswith('.js') and not file_name.lower().endswith('.min.js'):
skip_dirs = self._skip_javascript_dirs
elif file_name.lower().endswith('.coffee'):
skip_dirs = self._skip_coffeescript_dirs
else:
return results
if not self._is_valid_directory(skip_dirs, directory):
return results
return self._load_and_check_javascript_file_is_safe(file_full_path, results)
def _is_valid_directory(self, skip_dirs, directory):
""" """
Searches for Mako expressions and then checks if they contain Determines if the provided directory is a directory that could contain
violations. a JavaScript file that needs to be linted.
Arguments: Arguments:
mako_template: The contents of the Mako template. skip_dirs: The directories to be skipped.
has_page_default: True if the page is marked as default, False directory: The directory to be linted.
otherwise.
results: A list of results into which violations will be added.
Returns:
True if this directory should be linted for JavaScript violations
and False otherwise.
""" """
expressions = self._find_mako_expressions(mako_template) if _is_skip_dir(skip_dirs, directory):
contexts = self._get_contexts(mako_template) return False
for expression in expressions:
if expression['expression'] is None:
results.violations.append(ExpressionRuleViolation(
Rules.mako_unparseable_expression, expression
))
continue
context = self._get_context(contexts, expression['start_index']) return True
self._check_filters(mako_template, expression, context, has_page_default, results)
self._check_deprecated_display_name(expression, results)
self._check_html_and_text(expression, has_page_default, results)
def _check_deprecated_display_name(self, expression, results): def _load_and_check_javascript_file_is_safe(self, file_full_path, results):
""" """
Checks that the deprecated display_name_with_default_escaped is not Loads the JavaScript file and checks if it is in violation.
used. Adds violation to results if there is a problem.
Arguments: Arguments:
expression: A dict containing the start_index, end_index, and file_full_path: The file to be loaded and linted.
expression (text) of the expression.
results: A list of results into which violations will be added. Returns:
The file results containing any violations.
""" """
if '.display_name_with_default_escaped' in expression['expression']: file_contents = _load_file(file_full_path)
results.violations.append(ExpressionRuleViolation( self.check_javascript_file_is_safe(file_contents, results)
Rules.mako_deprecated_display_name, expression return results
))
def _check_html_and_text(self, expression, has_page_default, results): def check_javascript_file_is_safe(self, file_contents, results):
""" """
Checks rules related to proper use of HTML() and Text(). Checks for violations in a JavaScript file.
Arguments: Arguments:
expression: A dict containing the start_index, end_index, and file_contents: The contents of the JavaScript file.
expression (text) of the expression. results: A file results objects to which violations will be added.
has_page_default: True if the page is marked as default, False
otherwise.
results: A list of results into which violations will be added.
""" """
# strip '${' and '}' and whitespace from ends no_caller_check = None
expression_inner = expression['expression'][2:-1].strip() no_argument_check = None
# find the template relative inner expression start index self._check_jquery_function(
# - find used to take into account above strip() file_contents, "append", Rules.javascript_jquery_append, no_caller_check,
template_inner_start_index = expression['start_index'] + expression['expression'].find(expression_inner) self._is_jquery_argument_safe, results
if 'HTML(' in expression_inner: )
if expression_inner.startswith('HTML('): self._check_jquery_function(
close_paren_index = _find_closing_char_index( file_contents, "prepend", Rules.javascript_jquery_prepend, no_caller_check,
None, "(", ")", expression_inner, start_index=len('HTML(') self._is_jquery_argument_safe, results
)['close_char_index'] )
# check that the close paren is at the end of the stripped expression. self._check_jquery_function(
if close_paren_index != len(expression_inner) - 1: file_contents, "unwrap|wrap|wrapAll|wrapInner|after|before|replaceAll|replaceWith",
results.violations.append(ExpressionRuleViolation( Rules.javascript_jquery_insertion, no_caller_check, self._is_jquery_argument_safe, results
Rules.mako_html_alone, expression )
)) self._check_jquery_function(
elif expression_inner.startswith('Text(') is False: file_contents, "appendTo|prependTo|insertAfter|insertBefore",
results.violations.append(ExpressionRuleViolation( Rules.javascript_jquery_insert_into_target, self._is_jquery_insert_caller_safe, no_argument_check, results
Rules.mako_html_requires_text, expression )
)) self._check_jquery_function(
else: file_contents, "html", Rules.javascript_jquery_html, no_caller_check,
if 'Text(' in expression_inner: self._is_jquery_html_argument_safe, results
results.violations.append(ExpressionRuleViolation( )
Rules.mako_text_redundant, expression self._check_javascript_interpolate(file_contents, results)
)) self._check_javascript_escape(file_contents, results)
self._check_concat_with_html(file_contents, results)
# strings to be checked for HTML self.underScoreLinter.check_underscore_file_is_safe(file_contents, results)
unwrapped_html_strings = expression['strings'] results.prepare_results(file_contents, line_comment_delim='//')
for match in re.finditer(r"(HTML\(|Text\()", expression_inner):
result = _find_closing_char_index(None, "(", ")", expression_inner, start_index=match.end())
close_paren_index = result['close_char_index']
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
))
if match.group() == "HTML(":
# remove expression strings wrapped in HTML()
for string in list(unwrapped_html_strings):
html_inner_start_index = template_inner_start_index + match.end()
html_inner_end_index = template_inner_start_index + close_paren_index
if html_inner_start_index <= string.start_index and string.end_index <= html_inner_end_index:
unwrapped_html_strings.remove(string)
# check strings not wrapped in HTML() for '<'
for string in unwrapped_html_strings:
if '<' in string.string_inner:
results.violations.append(ExpressionRuleViolation(
Rules.mako_wrap_html, expression
))
break
# check strings not wrapped in HTML() for HTML entities
if has_page_default:
for string in unwrapped_html_strings:
if re.search(r"&[#]?[a-zA-Z0-9]+;", string.string_inner):
results.violations.append(ExpressionRuleViolation(
Rules.mako_html_entities, expression
))
break
def _check_filters(self, mako_template, expression, context, has_page_default, results): def _get_expression_for_function(self, file_contents, function_start_match):
""" """
Checks that the filters used in the given Mako expression are valid Returns an expression that matches the function call opened with
for the given context. Adds violation to results if there is a problem. function_start_match.
Arguments: Arguments:
mako_template: The contents of the Mako template. file_contents: The contents of the JavaScript file.
expression: A dict containing the start_index, end_index, and function_start_match: A regex match representing the start of the function
expression (text) of the expression. call (e.g. ".escape(").
context: The context of the page in which the expression was found
(e.g. javascript, html).
has_page_default: True if the page is marked as default, False
otherwise.
results: A list of results into which violations will be added.
"""
# finds "| n, h}" when given "${x | n, h}"
filters_regex = re.compile('\|[a-zA-Z_,\s]*\}')
filters_match = filters_regex.search(expression['expression'])
if filters_match is None:
if context == 'javascript':
results.violations.append(ExpressionRuleViolation(
Rules.mako_invalid_js_filter, expression
))
return
filters = filters_match.group()[1:-1].replace(" ", "").split(",")
if (len(filters) == 2) and (filters[0] == 'n') and (filters[1] == 'unicode'):
# {x | n, unicode} is valid in any context
pass
elif context == 'html':
if (len(filters) == 1) and (filters[0] == 'h'):
if has_page_default:
# suppress this violation if the page default hasn't been set,
# otherwise the template might get less safe
results.violations.append(ExpressionRuleViolation(
Rules.mako_unwanted_html_filter, expression
))
else:
results.violations.append(ExpressionRuleViolation(
Rules.mako_invalid_html_filter, expression
))
else: Returns:
if (len(filters) == 2) and (filters[0] == 'n') and (filters[1] == 'dump_js_escaped_json'): An Expression that best matches the function.
# {x | n, dump_js_escaped_json} is valid
pass
elif (len(filters) == 2) and (filters[0] == 'n') and (filters[1] == 'js_escaped_string'):
# {x | n, js_escaped_string} is valid, if surrounded by quotes
pass
else:
results.violations.append(ExpressionRuleViolation(
Rules.mako_invalid_js_filter, expression
))
def _get_contexts(self, mako_template):
""" """
Returns a data structure that represents the indices at which the start_index = function_start_match.start()
template changes from HTML context to JavaScript and back. inner_start_index = function_start_match.end()
result = _find_closing_char_index(
None, "(", ")", file_contents, start_index=inner_start_index
)
if result is not None:
end_index = result['close_char_index'] + 1
expression = Expression(
start_index, end_index, template=file_contents, start_delim=function_start_match.group(), end_delim=")"
)
else:
expression = Expression(start_index)
return expression
Return: def _check_javascript_interpolate(self, file_contents, results):
A list of dicts where each dict contains the 'index' of the context
and the context 'type' (e.g. 'html' or 'javascript').
""" """
contexts_re = re.compile(r""" Checks that interpolate() calls are safe.
<script.*?>| # script tag start
</script>| # script tag end
<%static:require_module.*?>| # require js script tag start
</%static:require_module> # require js script tag end""", re.VERBOSE | re.IGNORECASE)
media_type_re = re.compile(r"""type=['"].*?['"]""", re.IGNORECASE)
contexts = [{'index': 0, 'type': 'html'}]
for context in contexts_re.finditer(mako_template):
match_string = context.group().lower()
if match_string.startswith("<script"):
match_type = media_type_re.search(match_string)
context_type = 'javascript'
if match_type is not None:
# get media type (e.g. get text/javascript from
# type="text/javascript")
match_type = match_type.group()[6:-1].lower()
if match_type not in [
'text/javascript',
'text/ecmascript',
'application/ecmascript',
'application/javascript',
]:
#TODO: What are other types found, and are these really
# html? Or do we need to properly handle unknown
# contexts?
context_type = 'html'
contexts.append({'index': context.end(), 'type': context_type})
elif match_string.startswith("</script"):
contexts.append({'index': context.start(), 'type': 'html'})
elif match_string.startswith("<%static:require_module"):
contexts.append({'index': context.end(), 'type': 'javascript'})
else:
contexts.append({'index': context.start(), 'type': 'html'})
return contexts
def _get_context(self, contexts, index): Only use of StringUtils.interpolate() or HtmlUtils.interpolateText()
""" are safe.
Gets the context (e.g. javascript, html) of the template at the given
index.
Arguments: Arguments:
contexts: A list of dicts where each dict contains the 'index' of the context file_contents: The contents of the JavaScript file.
and the context 'type' (e.g. 'html' or 'javascript'). results: A file results objects to which violations will be added.
index: The index for which we want the context.
Returns:
The context (e.g. javascript or html) for the given index.
""" """
current_context = contexts[0]['type'] # Ignores calls starting with "StringUtils.", because those are safe
for context in contexts: regex = re.compile(r"(?<!StringUtils).interpolate\(")
if context['index'] <= index: for function_match in regex.finditer(file_contents):
current_context = context['type'] expression = self._get_expression_for_function(file_contents, function_match)
else: results.violations.append(ExpressionRuleViolation(Rules.javascript_interpolate, expression))
break
return current_context
def _find_mako_expressions(self, mako_template): def _check_javascript_escape(self, file_contents, results):
""" """
Finds all the Mako expressions in a Mako template and creates a list Checks that only necessary escape() are used.
of dicts for each expression.
Arguments:
mako_template: The content of the Mako template.
Returns: Allows for _.escape(), although this shouldn't be the recommendation.
A list of dicts for each expression, where the dict contains the
following:
start_index: The index of the start of the expression. Arguments:
end_index: The index immediately following the expression, or -1 file_contents: The contents of the JavaScript file.
if unparseable. results: A file results objects to which violations will be added.
expression: The text of the expression.
strings: a list of ParseStrings
""" """
start_delim = '${' # Ignores calls starting with "_.", because those are safe
start_index = 0 regex = regex = re.compile(r"(?<!_).escape\(")
expressions = [] for function_match in regex.finditer(file_contents):
expression = self._get_expression_for_function(file_contents, function_match)
while True: results.violations.append(ExpressionRuleViolation(Rules.javascript_escape, expression))
start_index = mako_template.find(start_delim, start_index)
if start_index < 0:
break
result = _find_closing_char_index(
start_delim, '{', '}', mako_template, start_index=start_index + len(start_delim)
)
close_char_index = result['close_char_index']
if close_char_index < 0:
expression = None
else:
expression = mako_template[start_index:close_char_index + 1]
expression = {
'start_index': start_index,
'end_index': close_char_index + 1,
'expression': expression,
'strings': result['strings'],
}
expressions.append(expression)
# end_index of -1 represents a parsing error and we may find others
start_index = max(start_index + len(start_delim), close_char_index)
return expressions
class UnderscoreTemplateLinter(object):
"""
The linter for Underscore.js template files.
"""
_skip_underscore_dirs = _skip_dirs + ('test',)
def process_file(self, directory, file_name): def _check_jquery_function(self, file_contents, function_names, rule, is_caller_safe, is_argument_safe, results):
""" """
Process file to determine if it is an Underscore template file and Checks that the JQuery function_names (e.g. append(), prepend()) calls
if it is safe. are safe.
Arguments: Arguments:
directory (string): The directory of the file to be checked file_contents: The contents of the JavaScript file.
file_name (string): A filename for a potential underscore file function_names: A pipe delimited list of names of the functions
(e.g. "wrap|after|before").
Returns: rule: The name of the rule to use for validation errors (e.g.
The file results containing any violations. Rules.javascript_jquery_append).
is_caller_safe: A function to test if caller of the JQuery function
is safe.
is_argument_safe: A function to test if the argument passed to the
JQuery function is safe.
results: A file results objects to which violations will be added.
""" """
full_path = os.path.normpath(directory + '/' + file_name) # Ignores calls starting with "HtmlUtils.", because those are safe
results = FileResults(full_path) regex = re.compile(r"(?<!HtmlUtils).(?:{})\(".format(function_names))
for function_match in regex.finditer(file_contents):
if not self._is_valid_directory(directory): is_violation = True
return results expression = self._get_expression_for_function(file_contents, function_match)
if expression.end_index is not None:
if not file_name.lower().endswith('.underscore'): start_index = expression.start_index
return results inner_start_index = function_match.end()
close_paren_index = expression.end_index - 1
return self._load_and_check_underscore_file_is_safe(full_path, results) function_argument = file_contents[inner_start_index:close_paren_index].strip()
if is_argument_safe is not None and is_caller_safe is None:
is_violation = is_argument_safe(function_argument) is False
elif is_caller_safe is not None and is_argument_safe is None:
line_start_index = StringLines(file_contents).index_to_line_start_index(start_index)
caller_line_start = file_contents[line_start_index:start_index]
is_violation = is_caller_safe(caller_line_start) is False
else:
raise ValueError("Must supply either is_argument_safe, or is_caller_safe, but not both.")
if is_violation:
results.violations.append(ExpressionRuleViolation(rule, expression))
def _is_valid_directory(self, directory): def _is_jquery_argument_safe_html_utils_call(self, argument):
""" """
Determines if the provided directory is a directory that could contain Checks that the argument sent to a jQuery DOM insertion function is a
Underscore.js template files that need to be linted. safe call to HtmlUtils.
A safe argument is of the form:
- HtmlUtils.xxx(anything).toString()
- edx.HtmlUtils.xxx(anything).toString()
Arguments: Arguments:
directory: The directory to be linted. argument: The argument sent to the jQuery function (e.g.
append(argument)).
Returns: Returns:
True if this directory should be linted for Underscore.js template True if the argument is safe, and False otherwise.
violations and False otherwise.
"""
if _is_skip_dir(self._skip_underscore_dirs, directory):
return False
return True """
# match on HtmlUtils.xxx().toString() or edx.HtmlUtils
match = re.search(r"(?:edx\.)?HtmlUtils\.[a-zA-Z0-9]+\(.*\)\.toString\(\)", argument)
return match is not None and match.group() == argument
def _load_and_check_underscore_file_is_safe(self, file_full_path, results): def _is_jquery_argument_safe(self, argument):
""" """
Loads the Underscore.js template file and checks if it is in violation. Check the argument sent to a jQuery DOM insertion function (e.g.
append()) to check if it is safe.
Safe arguments include:
- the argument can end with ".el", ".$el" (with no concatenation)
- the argument can be a single variable ending in "El" or starting with
"$". For example, "testEl" or "$test".
- the argument can be a single string literal with no HTML tags
- the argument can be a call to $() with the first argument a string
literal with a single HTML tag. For example, ".append($('<br/>'))"
or ".append($('<br/>'))".
- the argument can be a call to HtmlUtils.xxx(html).toString()
Arguments: Arguments:
file_full_path: The file to be loaded and linted argument: The argument sent to the jQuery function (e.g.
append(argument)).
Returns: Returns:
The file results containing any violations. True if the argument is safe, and False otherwise.
""" """
underscore_template = _load_file(self, file_full_path) match_variable_name = re.search("[_$a-zA-Z]+[_$a-zA-Z0-9]*", argument)
self.check_underscore_file_is_safe(underscore_template, results) if match_variable_name is not None and match_variable_name.group() == argument:
return results if argument.endswith('El') or argument.startswith('$'):
return True
elif argument.startswith('"') or argument.startswith("'"):
# a single literal string with no HTML is ok
# 1. it gets rid of false negatives for non-jquery calls (e.g. graph.append("g"))
# 2. JQuery will treat this as a plain text string and will escape any & if needed.
string = ParseString(argument, 0, len(argument))
if string.string == argument and "<" not in argument:
return True
elif argument.startswith('$('):
# match on JQuery calls with single string and single HTML tag
# Examples:
# $("<span>")
# $("<div/>")
# $("<div/>", {...})
match = re.search(r"""\$\(\s*['"]<[a-zA-Z0-9]+\s*[/]?>['"]\s*[,)]""", argument)
if match is not None:
return True
elif self._is_jquery_argument_safe_html_utils_call(argument):
return True
# check rules that shouldn't use concatenation
elif "+" not in argument:
if argument.endswith('.el') or argument.endswith('.$el'):
return True
return False
def check_underscore_file_is_safe(self, underscore_template, results): def _is_jquery_html_argument_safe(self, argument):
""" """
Checks for violations in an Underscore.js template. Check the argument sent to the jQuery html() function to check if it is
safe.
Safe arguments to html():
- no argument (i.e. getter rather than setter)
- empty string is safe
- the argument can be a call to HtmlUtils.xxx(html).toString()
Arguments: Arguments:
underscore_template: The contents of the Underscore.js template. argument: The argument sent to html() in code (i.e. html(argument)).
results: A file results objects to which violations will be added.
""" Returns:
self._check_underscore_expressions(underscore_template, results) True if the argument is safe, and False otherwise.
results.prepare_results(underscore_template)
def _check_underscore_expressions(self, underscore_template, results):
""" """
Searches for Underscore.js expressions that contain violations. if argument == "" or argument == "''" or argument == '""':
return True
Arguments: elif self._is_jquery_argument_safe_html_utils_call(argument):
underscore_template: The contents of the Underscore.js template. return True
results: A list of results into which violations will be added. return False
def _is_jquery_insert_caller_safe(self, caller_line_start):
""" """
expressions = self._find_unescaped_expressions(underscore_template) Check that the caller of a jQuery DOM insertion function that takes a
for expression in expressions: target is safe (e.g. thisEl.appendTo(target)).
if not self._is_safe_unescaped_expression(expression):
results.violations.append(ExpressionRuleViolation(
Rules.underscore_not_escaped, expression
))
def _is_safe_unescaped_expression(self, expression): If original line was::
"""
Determines whether an expression is safely escaped, even though it is
using the expression syntax that doesn't itself escape (i.e. <%= ).
In some cases it is ok to not use the Underscore.js template escape draggableObj.iconEl.appendTo(draggableObj.containerEl);
(i.e. <%- ) because the escaping is happening inside the expression.
Safe examples:: Parameter caller_line_start would be:
<%= HtmlUtils.ensureHtml(message) %> draggableObj.iconEl
<%= _.escape(message) %>
Safe callers include:
- the caller can be ".el", ".$el"
- the caller can be a single variable ending in "El" or starting with
"$". For example, "testEl" or "$test".
Arguments: Arguments:
expression: The expression being checked. caller_line_start: The line leading up to the jQuery function call.
Returns: Returns:
True if the expression has been safely escaped, and False otherwise. True if the caller is safe, and False otherwise.
""" """
if expression['expression_inner'].startswith('HtmlUtils.'): # matches end of line for caller, which can't itself be a function
caller_match = re.search(r"(?:\s*|[.])([_$a-zA-Z]+[_$a-zA-Z0-9])*$", caller_line_start)
if caller_match is None:
return False
caller = caller_match.group(1)
if caller is None:
return False
elif caller.endswith('El') or caller.startswith('$'):
return True return True
if expression['expression_inner'].startswith('_.escape('): elif caller == 'el' or caller == 'parentNode':
return True return True
return False return False
def _find_unescaped_expressions(self, underscore_template): def _check_concat_with_html(self, file_contents, results):
""" """
Returns a list of unsafe expressions. Checks that strings with HTML are not concatenated
At this time all expressions that are unescaped are considered unsafe.
Arguments: Arguments:
underscore_template: The contents of the Underscore.js template. file_contents: The contents of the JavaScript file.
results: A file results objects to which violations will be added.
Returns:
A list of dicts for each expression, where the dict contains the
following:
start_index: The index of the start of the expression.
end_index: The index of the end of the expression.
expression: The text of the expression.
""" """
unescaped_expression_regex = re.compile("<%=(.*?)%>", re.DOTALL) lines = StringLines(file_contents)
last_expression = None
expressions = [] # attempt to match a string that starts with '<' or ends with '>'
for match in unescaped_expression_regex.finditer(underscore_template): regex_string_with_html = r"""["'](?:\s*<.*|.*>\s*)["']"""
expression = { regex_concat_with_html = r"(\+\s*{}|{}\s*\+)".format(regex_string_with_html, regex_string_with_html)
'start_index': match.start(), for match in re.finditer(regex_concat_with_html, file_contents):
'end_index': match.end(), found_new_violation = False
'expression': match.group(), if last_expression is not None:
'expression_inner': match.group(1).strip() last_line = lines.index_to_line_number(last_expression.start_index)
} # check if violation should be expanded to more of the same line
expressions.append(expression) if last_line == lines.index_to_line_number(match.start()):
last_expression = Expression(
last_expression.start_index, match.end(), template=file_contents
)
else:
results.violations.append(ExpressionRuleViolation(
Rules.javascript_concat_html, last_expression
))
found_new_violation = True
else:
found_new_violation = True
if found_new_violation:
last_expression = Expression(
match.start(), match.end(), template=file_contents
)
return expressions # add final expression
if last_expression is not None:
results.violations.append(ExpressionRuleViolation(
Rules.javascript_concat_html, last_expression
))
class JavaScriptLinter(object): class MakoTemplateLinter(object):
""" """
The linter for JavaScript and CoffeeScript files. The linter for Mako template files.
""" """
_skip_javascript_dirs = _skip_dirs + ('i18n', 'static/coffee') _skip_mako_dirs = _skip_dirs
_skip_coffeescript_dirs = _skip_dirs javaScriptLinter = JavaScriptLinter()
underScoreLinter = UnderscoreTemplateLinter()
def process_file(self, directory, file_name): def process_file(self, directory, file_name):
""" """
Process file to determine if it is a JavaScript file and Process file to determine if it is a Mako template file and
if it is safe. if it is safe.
Arguments: Arguments:
directory (string): The directory of the file to be checked directory (string): The directory of the file to be checked
file_name (string): A filename for a potential JavaScript file file_name (string): A filename for a potential Mako file
Returns: Returns:
The file results containing any violations. The file results containing any violations.
""" """
file_full_path = os.path.normpath(directory + '/' + file_name) mako_file_full_path = os.path.normpath(directory + '/' + file_name)
results = FileResults(file_full_path) results = FileResults(mako_file_full_path)
if not results.is_file: if not results.is_file:
return results return results
if file_name.lower().endswith('.js') and not file_name.lower().endswith('.min.js'): if not self._is_valid_directory(directory):
skip_dirs = self._skip_javascript_dirs
elif file_name.lower().endswith('.coffee'):
skip_dirs = self._skip_coffeescript_dirs
else:
return results return results
if not self._is_valid_directory(skip_dirs, directory): # TODO: When safe-by-default is turned on at the platform level, will we:
# 1. Turn it on for .html only, or
# 2. Turn it on for all files, and have different rulesets that have
# different rules of .xml, .html, .js, .txt Mako templates (e.g. use
# the n filter to turn off h for some of these)?
# For now, we only check .html and .xml files
if not (file_name.lower().endswith('.html') or file_name.lower().endswith('.xml')):
return results return results
return self._load_and_check_javascript_file_is_safe(file_full_path, results) return self._load_and_check_mako_file_is_safe(mako_file_full_path, results)
def _is_valid_directory(self, skip_dirs, directory): def _is_valid_directory(self, directory):
""" """
Determines if the provided directory is a directory that could contain Determines if the provided directory is a directory that could contain
a JavaScript file that needs to be linted. Mako template files that need to be linted.
Arguments: Arguments:
skip_dirs: The directories to be skipped.
directory: The directory to be linted. directory: The directory to be linted.
Returns: Returns:
True if this directory should be linted for JavaScript violations True if this directory should be linted for Mako template violations
and False otherwise. and False otherwise.
""" """
if _is_skip_dir(skip_dirs, directory): if _is_skip_dir(self._skip_mako_dirs, directory):
return False return False
return True # TODO: This is an imperfect guess concerning the Mako template
# directories. This needs to be reviewed before turning on safe by
# default at the platform level.
if ('/templates/' in directory) or directory.endswith('/templates'):
return True
def _load_and_check_javascript_file_is_safe(self, file_full_path, results): return False
def _load_and_check_mako_file_is_safe(self, mako_file_full_path, results):
""" """
Loads the JavaScript file and checks if it is in violation. Loads the Mako template file and checks if it is in violation.
Arguments: Arguments:
file_full_path: The file to be loaded and linted. mako_file_full_path: The file to be loaded and linted.
Returns: Returns:
The file results containing any violations. The file results containing any violations.
""" """
file_contents = _load_file(self, file_full_path) mako_template = _load_file(mako_file_full_path)
self._check_javascript_file_is_safe(file_contents, results) self._check_mako_file_is_safe(mako_template, results)
return results return results
def _check_javascript_file_is_safe(self, file_contents, results): def _check_mako_file_is_safe(self, mako_template, results):
""" """
Checks for violations in a JavaScript file. Checks for violations in a Mako template.
Arguments: Arguments:
file_contents: The contents of the JavaScript file. mako_template: The contents of the Mako template.
results: A file results objects to which violations will be added. results: A file results objects to which violations will be added.
""" """
no_caller_check = None if self._is_django_template(mako_template):
no_argument_check = None return
self._check_jquery_function( has_page_default = self._has_page_default(mako_template, results)
file_contents, "append", Rules.javascript_jquery_append, no_caller_check, self._check_mako_expressions(mako_template, has_page_default, results)
self._is_jquery_argument_safe, results results.prepare_results(mako_template, line_comment_delim='##')
)
self._check_jquery_function(
file_contents, "prepend", Rules.javascript_jquery_prepend, no_caller_check,
self._is_jquery_argument_safe, results
)
self._check_jquery_function(
file_contents, "unwrap|wrap|wrapAll|wrapInner|after|before|replaceAll|replaceWith",
Rules.javascript_jquery_insertion, no_caller_check, self._is_jquery_argument_safe, results
)
self._check_jquery_function(
file_contents, "appendTo|prependTo|insertAfter|insertBefore",
Rules.javascript_jquery_insert_into_target, self._is_jquery_insert_caller_safe, no_argument_check, results
)
self._check_jquery_function(
file_contents, "html", Rules.javascript_jquery_html, no_caller_check,
self._is_jquery_html_argument_safe, results
)
self._check_javascript_interpolate(file_contents, results)
self._check_javascript_escape(file_contents, results)
self._check_concat_with_html(file_contents, results)
self.underScoreLinter.check_underscore_file_is_safe(file_contents, results)
results.prepare_results(file_contents)
def _get_expression_for_function(self, file_contents, function_match): def _is_django_template(self, mako_template):
""" """
Returns an expression that best matches the function call. Determines if the template is actually a Django template.
Arguments: Arguments:
file_contents: The contents of the JavaScript file. mako_template: The template code.
function_match: A regex match representing the start of the function
call. Returns:
True if this is really a Django template, and False otherwise.
""" """
start_index = function_match.start() if re.search('({%.*%})|({{.*}})', mako_template) is not None:
inner_start_index = function_match.end() return True
close_paren_index = _find_closing_char_index( return False
None, "(", ")", file_contents, start_index=inner_start_index
)['close_char_index']
if 0 <= close_paren_index:
end_index = close_paren_index + 1
expression_text = file_contents[function_match.start():close_paren_index + 1]
expression = {
'start_index': start_index,
'end_index': end_index,
'expression': expression_text,
'expression_inner': expression_text,
}
else:
expression = {
'start_index': start_index,
'end_index': -1,
'expression': None,
'expression_inner': None,
}
return expression
def _check_javascript_interpolate(self, file_contents, results): def _get_page_tag_count(self, mako_template):
""" """
Checks that interpolate() calls are safe. Determines the number of page expressions in the Mako template. Ignores
page expressions that are commented out.
Only use of StringUtils.interpolate() or HtmlUtils.interpolateText() Arguments:
are safe. mako_template: The contents of the Mako template.
Returns:
The number of page expressions
"""
count = len(re.findall('<%page ', mako_template, re.IGNORECASE))
count_commented = len(re.findall(r'##\s+<%page ', mako_template, re.IGNORECASE))
return max(0, count - count_commented)
def _has_page_default(self, mako_template, results):
"""
Checks if the Mako template contains the page expression marking it as
safe by default.
Arguments: Arguments:
file_contents: The contents of the JavaScript file. mako_template: The contents of the Mako template.
results: A file results objects to which violations will be added. results: A list of results into which violations will be added.
Side effect:
Adds violations regarding page default if necessary
Returns:
True if the template has the page default, and False otherwise.
""" """
# Ignores calls starting with "StringUtils.", because those are safe page_tag_count = self._get_page_tag_count(mako_template)
regex = re.compile(r"(?<!StringUtils).interpolate\(") # check if there are too many page expressions
for function_match in regex.finditer(file_contents): if 2 <= page_tag_count:
expression = self._get_expression_for_function(file_contents, function_match) results.violations.append(RuleViolation(Rules.mako_multiple_page_tags))
results.violations.append(ExpressionRuleViolation(Rules.javascript_interpolate, expression)) return False
# make sure there is exactly 1 page expression, excluding commented out
# page expressions, before proceeding
elif page_tag_count != 1:
results.violations.append(RuleViolation(Rules.mako_missing_default))
return False
# check that safe by default (h filter) is turned on
page_h_filter_regex = re.compile('<%page[^>]*expression_filter=(?:"h"|\'h\')[^>]*/>')
page_match = page_h_filter_regex.search(mako_template)
if not page_match:
results.violations.append(RuleViolation(Rules.mako_missing_default))
return page_match
def _check_javascript_escape(self, file_contents, results): def _check_mako_expressions(self, mako_template, has_page_default, results):
""" """
Checks that only necessary escape() are used. Searches for Mako expressions and then checks if they contain
violations, including checking JavaScript contexts for JavaScript
violations.
Allows for _.escape(), although this shouldn't be the recommendation. Arguments:
mako_template: The contents of the Mako template.
has_page_default: True if the page is marked as default, False
otherwise.
results: A list of results into which violations will be added.
"""
expressions = self._find_mako_expressions(mako_template)
contexts = self._get_contexts(mako_template)
self._check_javascript_contexts(mako_template, contexts, results)
for expression in expressions:
if expression.end_index is None:
results.violations.append(ExpressionRuleViolation(
Rules.mako_unparseable_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, has_page_default, results)
def _check_javascript_contexts(self, mako_template, contexts, results):
"""
Lint the JavaScript contexts for JavaScript violations inside a Mako
template.
Arguments: Arguments:
file_contents: The contents of the JavaScript file. mako_template: The contents of the Mako template.
results: A file results objects to which violations will be added. contexts: A list of context dicts with 'type' and 'index'.
results: A list of results into which violations will be added.
Side effect:
Adds JavaScript violations to results.
""" """
# Ignores calls starting with "_.", because those are safe javascript_start_index = None
regex = regex = re.compile(r"(?<!_).escape\(") for context in contexts:
for function_match in regex.finditer(file_contents): if context['type'] == 'javascript':
expression = self._get_expression_for_function(file_contents, function_match) if javascript_start_index < 0:
results.violations.append(ExpressionRuleViolation(Rules.javascript_escape, expression)) javascript_start_index = context['index']
else:
if javascript_start_index is not None:
javascript_end_index = context['index']
javascript_code = mako_template[javascript_start_index:javascript_end_index]
self._check_javascript_context(javascript_code, javascript_start_index, results)
javascript_start_index = None
if javascript_start_index is not None:
javascript_code = mako_template[javascript_start_index:]
self._check_javascript_context(javascript_code, javascript_start_index, results)
def _check_jquery_function(self, file_contents, function_names, rule, is_caller_safe, is_argument_safe, results): def _check_javascript_context(self, javascript_code, start_offset, results):
""" """
Checks that the JQuery function_names (e.g. append(), prepend()) calls Lint a single JavaScript context for JavaScript violations inside a Mako
are safe. template.
Arguments: Arguments:
file_contents: The contents of the JavaScript file. javascript_code: The template contents of the JavaScript context.
function_names: A pipe delimited list of names of the functions start_offset: The offset of the JavaScript context inside the
(e.g. "wrap|after|before"). original Mako template.
rule: The name of the rule to use for validation errors (e.g. results: A list of results into which violations will be added.
Rules.javascript_jquery_append).
is_caller_safe: A function to test if caller of the JQuery function Side effect:
is safe. Adds JavaScript violations to results.
is_argument_safe: A function to test if the argument passed to the
JQuery function is safe.
results: A file results objects to which violations will be added.
""" """
# Ignores calls starting with "HtmlUtils.", because those are safe javascript_results = FileResults("")
regex = re.compile(r"(?<!HtmlUtils).(?:{})\(".format(function_names)) self.javaScriptLinter.check_javascript_file_is_safe(javascript_code, javascript_results)
for function_match in regex.finditer(file_contents): # translate the violations into the location within the original
is_violation = True # Mako template
expression = self._get_expression_for_function(file_contents, function_match) for violation in javascript_results.violations:
if 0 < expression['end_index']: expression = violation.expression
start_index = expression['start_index'] expression.start_index += start_offset
inner_start_index = function_match.end() if expression.end_index is not None:
close_paren_index = expression['end_index'] - 1 expression.end_index += start_offset
function_argument = file_contents[inner_start_index:close_paren_index].strip() results.violations.append(ExpressionRuleViolation(violation.rule, expression))
if is_argument_safe is not None and is_caller_safe is None:
is_violation = is_argument_safe(function_argument) is False
elif is_caller_safe is not None and is_argument_safe is None:
line_start_index = StringLines(file_contents).index_to_line_start_index(start_index)
caller_line_start = file_contents[line_start_index:start_index]
is_violation = is_caller_safe(caller_line_start) is False
else:
raise ValueError("Must supply either is_argument_safe, or is_caller_safe, but not both.")
if is_violation:
results.violations.append(ExpressionRuleViolation(rule, expression))
def _is_jquery_argument_safe_html_utils_call(self, argument): def _check_deprecated_display_name(self, expression, results):
""" """
Checks that the argument sent to a jQuery DOM insertion function is a Checks that the deprecated display_name_with_default_escaped is not
safe call to HtmlUtils. used. Adds violation to results if there is a problem.
A safe argument is of the form: Arguments:
- HtmlUtils.xxx(anything).toString() expression: An Expression
- edx.HtmlUtils.xxx(anything).toString() results: A list of results into which violations will be added.
"""
if '.display_name_with_default_escaped' in expression.expression:
results.violations.append(ExpressionRuleViolation(
Rules.mako_deprecated_display_name, expression
))
def _check_html_and_text(self, expression, has_page_default, results):
"""
Checks rules related to proper use of HTML() and Text().
Arguments: Arguments:
argument: The argument sent to the jQuery function (e.g. expression: A Mako Expression.
append(argument)). has_page_default: True if the page is marked as default, False
otherwise.
results: A list of results into which violations will be added.
"""
expression_inner = expression.expression_inner
# use find to get the template relative inner expression start index
# due to possible skipped white space
template_inner_start_index = expression.start_index
template_inner_start_index += expression.expression.find(expression_inner)
if 'HTML(' in expression_inner:
if expression_inner.startswith('HTML('):
close_paren_index = _find_closing_char_index(
None, "(", ")", expression_inner, start_index=len('HTML(')
)['close_char_index']
# check that the close paren is at the end of the stripped 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
))
# strings to be checked for HTML
unwrapped_html_strings = expression.strings
for match in re.finditer(r"(HTML\(|Text\()", expression_inner):
result = _find_closing_char_index(None, "(", ")", expression_inner, start_index=match.end())
if result is not None:
close_paren_index = result['close_char_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
))
if match.group() == "HTML(":
# remove expression strings wrapped in HTML()
for string in list(unwrapped_html_strings):
html_inner_start_index = template_inner_start_index + match.end()
html_inner_end_index = template_inner_start_index + close_paren_index
if html_inner_start_index <= string.start_index and string.end_index <= html_inner_end_index:
unwrapped_html_strings.remove(string)
# check strings not wrapped in HTML() for '<'
for string in unwrapped_html_strings:
if '<' in string.string_inner:
results.violations.append(ExpressionRuleViolation(
Rules.mako_wrap_html, expression
))
break
# check strings not wrapped in HTML() for HTML entities
if has_page_default:
for string in unwrapped_html_strings:
if re.search(r"&[#]?[a-zA-Z0-9]+;", string.string_inner):
results.violations.append(ExpressionRuleViolation(
Rules.mako_html_entities, expression
))
break
def _check_filters(self, mako_template, expression, context, has_page_default, results):
"""
Checks that the filters used in the given Mako expression are valid
for the given context. Adds violation to results if there is a problem.
Returns: Arguments:
True if the argument is safe, and False otherwise. mako_template: The contents of the Mako template.
expression: A Mako Expression.
context: The context of the page in which the expression was found
(e.g. javascript, html).
has_page_default: True if the page is marked as default, False
otherwise.
results: A list of results into which violations will be added.
""" """
# match on HtmlUtils.xxx().toString() or edx.HtmlUtils # Example: finds "| n, h}" when given "${x | n, h}"
match = re.search(r"(?:edx\.)?HtmlUtils\.[a-zA-Z0-9]+\(.*\)\.toString\(\)", argument) filters_regex = re.compile(r'\|([.,\w\s]*)\}')
return match is not None and match.group() == argument filters_match = filters_regex.search(expression.expression)
if filters_match is None:
if context == 'javascript':
results.violations.append(ExpressionRuleViolation(
Rules.mako_invalid_js_filter, expression
))
return
def _is_jquery_argument_safe(self, argument): filters = filters_match.group(1).replace(" ", "").split(",")
""" if filters == ['n', 'decode.utf8']:
Check the argument sent to a jQuery DOM insertion function (e.g. # {x | n, decode.utf8} is valid in any context
append()) to check if it is safe. pass
elif context == 'html':
if filters == ['h']:
if has_page_default:
# suppress this violation if the page default hasn't been set,
# otherwise the template might get less safe
results.violations.append(ExpressionRuleViolation(
Rules.mako_unwanted_html_filter, expression
))
else:
results.violations.append(ExpressionRuleViolation(
Rules.mako_invalid_html_filter, expression
))
elif context == 'javascript':
self._check_js_expression_not_with_html(mako_template, expression, results)
if filters == ['n', 'dump_js_escaped_json']:
# {x | n, dump_js_escaped_json} is valid
pass
elif filters == ['n', 'js_escaped_string']:
# {x | n, js_escaped_string} is valid, if surrounded by quotes
self._check_js_string_expression_in_quotes(mako_template, expression, results)
else:
results.violations.append(ExpressionRuleViolation(
Rules.mako_invalid_js_filter, expression
))
Safe arguments include: def _check_js_string_expression_in_quotes(self, mako_template, expression, results):
- the argument can end with ".el", ".$el" (with no concatenation) """
- the argument can be a single variable ending in "El" or starting with Checks that a Mako expression using js_escaped_string is surrounded by
"$". For example, "testEl" or "$test". quotes.
- the argument can be a single string literal with no HTML tags
- the argument can be a call to $() with the first argument a string
literal with a single HTML tag. For example, ".append($('<br/>'))"
or ".append($('<br/>'))".
- the argument can be a call to HtmlUtils.xxx(html).toString()
Arguments: Arguments:
argument: The argument sent to the jQuery function (e.g. mako_template: The contents of the Mako template.
append(argument)). expression: A Mako Expression.
results: A list of results into which violations will be added.
Returns: """
True if the argument is safe, and False otherwise. parse_string = self._find_string_wrapping_expression(mako_template, expression)
if parse_string is None:
results.violations.append(ExpressionRuleViolation(
Rules.mako_js_missing_quotes, expression
))
def _check_js_expression_not_with_html(self, mako_template, expression, results):
""" """
match_variable_name = re.search("[_$a-zA-Z]+[_$a-zA-Z0-9]*", argument) Checks that a Mako expression in a JavaScript context does not appear in
if match_variable_name is not None and match_variable_name.group() == argument: a string that also contains HTML.
if argument.endswith('El') or argument.startswith('$'):
return True
elif argument.startswith('"') or argument.startswith("'"):
# a single literal string with no HTML is ok
# 1. it gets rid of false negatives for non-jquery calls (e.g. graph.append("g"))
# 2. JQuery will treat this as a plain text string and will escape any & if needed.
string = ParseString(argument, 0, len(argument))
if string.string == argument and "<" not in argument:
return True
elif argument.startswith('$('):
# match on JQuery calls with single string and single HTML tag
# Examples:
# $("<span>")
# $("<div/>")
# $("<div/>", {...})
match = re.search(r"""\$\(\s*['"]<[a-zA-Z0-9]+\s*[/]?>['"]\s*[,)]""", argument)
if match is not None:
return True
elif self._is_jquery_argument_safe_html_utils_call(argument):
return True
# check rules that shouldn't use concatenation
elif "+" not in argument:
if argument.endswith('.el') or argument.endswith('.$el'):
return True
return False
def _is_jquery_html_argument_safe(self, argument): Arguments:
mako_template: The contents of the Mako template.
expression: A Mako Expression.
results: A list of results into which violations will be added.
""" """
Check the argument sent to the jQuery html() function to check if it is parse_string = self._find_string_wrapping_expression(mako_template, expression)
safe. if parse_string is not None and re.search('[<>]', parse_string.string) is not None:
results.violations.append(ExpressionRuleViolation(
Rules.mako_js_html_string, expression
))
Safe arguments to html(): def _find_string_wrapping_expression(self, mako_template, expression):
- no argument (i.e. getter rather than setter) """
- empty string is safe Finds the string wrapping the Mako expression if there is one.
- the argument can be a call to HtmlUtils.xxx(html).toString()
Arguments: Arguments:
argument: The argument sent to html() in code (i.e. html(argument)). mako_template: The contents of the Mako template.
expression: A Mako Expression.
Returns: Returns:
True if the argument is safe, and False otherwise. ParseString representing a scrubbed version of the wrapped string,
where the Mako expression was replaced with "${...}", if a wrapped
string was found. Otherwise, returns None if none found.
"""
lines = StringLines(mako_template)
start_index = lines.index_to_line_start_index(expression.start_index)
if expression.end_index is not None:
end_index = lines.index_to_line_end_index(expression.end_index)
else:
return None
# scrub out the actual expression so any code inside the expression
# doesn't interfere with rules applied to the surrounding code (i.e.
# checking JavaScript).
scrubbed_lines = "".join((
mako_template[start_index:expression.start_index],
"${...}",
mako_template[expression.end_index:end_index]
))
adjusted_start_index = expression.start_index - start_index
start_index = 0
while True:
parse_string = ParseString(scrubbed_lines, start_index, len(scrubbed_lines))
# check for validly parsed string
if 0 <= parse_string.start_index < parse_string.end_index:
# check if expression is contained in the given string
if parse_string.start_index < adjusted_start_index < parse_string.end_index:
return parse_string
else:
# move to check next string
start_index = parse_string.end_index
else:
break
return None
def _get_contexts(self, mako_template):
""" """
if argument == "" or argument == "''" or argument == '""': Returns a data structure that represents the indices at which the
return True template changes from HTML context to JavaScript and back.
elif self._is_jquery_argument_safe_html_utils_call(argument):
return True
return False
def _is_jquery_insert_caller_safe(self, caller_line_start): Return:
A list of dicts where each dict contains:
- index: the index of the context.
- type: the context type (e.g. 'html' or 'javascript').
""" """
Check that the caller of a jQuery DOM insertion function that takes a contexts_re = re.compile(r"""
target is safe (e.g. thisEl.appendTo(target)). <script.*?>| # script tag start
</script>| # script tag end
If original line was:: <%static:require_module.*?>| # require js script tag start
</%static:require_module> # require js script tag end""", re.VERBOSE | re.IGNORECASE)
draggableObj.iconEl.appendTo(draggableObj.containerEl); media_type_re = re.compile(r"""type=['"].*?['"]""", re.IGNORECASE)
Parameter caller_line_start would be: contexts = [{'index': 0, 'type': 'html'}]
javascript_types = ['text/javascript', 'text/ecmascript', 'application/ecmascript', 'application/javascript']
for context in contexts_re.finditer(mako_template):
match_string = context.group().lower()
if match_string.startswith("<script"):
match_type = media_type_re.search(match_string)
context_type = 'javascript'
if match_type is not None:
# get media type (e.g. get text/javascript from
# type="text/javascript")
match_type = match_type.group()[6:-1].lower()
if match_type not in javascript_types:
# TODO: What are other types found, and are these really
# html? Or do we need to properly handle unknown
# contexts? Only "text/template" is a known alternative.
context_type = 'html'
contexts.append({'index': context.end(), 'type': context_type})
elif match_string.startswith("</script"):
contexts.append({'index': context.start(), 'type': 'html'})
elif match_string.startswith("<%static:require_module"):
contexts.append({'index': context.end(), 'type': 'javascript'})
else:
contexts.append({'index': context.start(), 'type': 'html'})
draggableObj.iconEl return contexts
Safe callers include: def _get_context(self, contexts, index):
- the caller can be ".el", ".$el" """
- the caller can be a single variable ending in "El" or starting with Gets the context (e.g. javascript, html) of the template at the given
"$". For example, "testEl" or "$test". index.
Arguments: Arguments:
caller_line_start: The line leading up to the jQuery function call. contexts: A list of dicts where each dict contains the 'index' of the context
and the context 'type' (e.g. 'html' or 'javascript').
index: The index for which we want the context.
Returns: Returns:
True if the caller is safe, and False otherwise. The context (e.g. javascript or html) for the given index.
""" """
# matches end of line for caller, which can't itself be a function current_context = contexts[0]['type']
caller_match = re.search(r"(?:\s*|[.])([_$a-zA-Z]+[_$a-zA-Z0-9])*$", caller_line_start) for context in contexts:
if caller_match is None: if context['index'] <= index:
return False current_context = context['type']
caller = caller_match.group(1) else:
if caller is None: break
return False return current_context
elif caller.endswith('El') or caller.startswith('$'):
return True
elif caller == 'el' or caller == 'parentNode':
return True
return False
def _check_concat_with_html(self, file_contents, results): def _find_mako_expressions(self, mako_template):
""" """
Checks that strings with HTML are not concatenated Finds all the Mako expressions in a Mako template and creates a list
of dicts for each expression.
Arguments: Arguments:
file_contents: The contents of the JavaScript file. mako_template: The content of the Mako template.
results: A file results objects to which violations will be added.
Returns:
A list of Expressions.
""" """
lines = StringLines(file_contents) start_delim = '${'
last_expression = None start_index = 0
# attempt to match a string that starts with '<' or ends with '>' expressions = []
regex_string_with_html = r"""["'](?:\s*<.*|.*>\s*)["']"""
regex_concat_with_html = r"(\+\s*{}|{}\s*\+)".format(regex_string_with_html, regex_string_with_html)
for match in re.finditer(regex_concat_with_html, file_contents):
found_new_violation = False
if last_expression is not None:
last_line = lines.index_to_line_number(last_expression['start_index'])
# check if violation should be expanded to more of the same line
if last_line == lines.index_to_line_number(match.start()):
last_expression['end_index'] = match.end()
else:
results.violations.append(ExpressionRuleViolation(
Rules.javascript_concat_html, last_expression
))
found_new_violation = True
else:
found_new_violation = True
if found_new_violation:
last_expression = {
'start_index': match.start(),
'end_index': match.end(),
}
# add final expression while True:
if last_expression is not None: start_index = mako_template.find(start_delim, start_index)
results.violations.append(ExpressionRuleViolation( if start_index < 0:
Rules.javascript_concat_html, last_expression break
))
result = _find_closing_char_index(
start_delim, '{', '}', mako_template, start_index=start_index + len(start_delim)
)
if result is None:
expression = Expression(start_index)
# for parsing error, restart search right after the start of the
# current expression
start_index = start_index + len(start_delim)
else:
close_char_index = result['close_char_index']
expression = mako_template[start_index:close_char_index + 1]
expression = Expression(
start_index,
end_index=close_char_index + 1,
template=mako_template,
start_delim=start_delim,
end_delim='}',
strings=result['strings'],
)
# restart search after the current expression
start_index = expression.end_index
expressions.append(expression)
return expressions
def _process_file(full_path, template_linters, options, out): def _process_file(full_path, template_linters, options, out):
...@@ -1699,12 +1966,17 @@ def _process_file(full_path, template_linters, options, out): ...@@ -1699,12 +1966,17 @@ def _process_file(full_path, template_linters, options, out):
options: A list of the options. options: A list of the options.
out: output file out: output file
Returns:
The number of violations.
""" """
num_violations = 0
directory = os.path.dirname(full_path) directory = os.path.dirname(full_path)
file = os.path.basename(full_path) file_name = os.path.basename(full_path)
for template_linter in template_linters: for template_linter in template_linters:
results = template_linter.process_file(directory, file) results = template_linter.process_file(directory, file_name)
results.print_results(options, out) num_violations += results.print_results(options, out)
return num_violations
def _process_current_walk(current_walk, template_linters, options, out): def _process_current_walk(current_walk, template_linters, options, out):
...@@ -1718,12 +1990,17 @@ def _process_current_walk(current_walk, template_linters, options, out): ...@@ -1718,12 +1990,17 @@ def _process_current_walk(current_walk, template_linters, options, out):
options: A list of the options. options: A list of the options.
out: output file out: output file
Returns:
The number of violations.
""" """
num_violations = 0
walk_directory = os.path.normpath(current_walk[0]) walk_directory = os.path.normpath(current_walk[0])
walk_files = current_walk[2] walk_files = current_walk[2]
for walk_file in walk_files: for walk_file in walk_files:
full_path = os.path.join(walk_directory, walk_file) full_path = os.path.join(walk_directory, walk_file)
_process_file(full_path, template_linters, options, out) num_violations += _process_file(full_path, template_linters, options, out)
return num_violations
def _process_os_walk(starting_dir, template_linters, options, out): def _process_os_walk(starting_dir, template_linters, options, out):
...@@ -1736,29 +2013,14 @@ def _process_os_walk(starting_dir, template_linters, options, out): ...@@ -1736,29 +2013,14 @@ def _process_os_walk(starting_dir, template_linters, options, out):
options: A list of the options. options: A list of the options.
out: output file out: output file
"""
for current_walk in os.walk(starting_dir):
_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: Returns:
The option value for a match, or None if arg is not for this option The number of violations.
""" """
if arg.startswith('--{}='.format(option)): num_violations = 0
option_value = arg.split('=')[1] for current_walk in os.walk(starting_dir):
if option_value.startswith("'") or option_value.startswith('"'): num_violations += _process_current_walk(current_walk, template_linters, options, out)
option_value = option_value[1:-1] return num_violations
return option_value
else:
return None
def main(): def main():
...@@ -1790,11 +2052,16 @@ def main(): ...@@ -1790,11 +2052,16 @@ def main():
if args.file is not None: if args.file is not None:
if os.path.isfile(args.file[0]) is False: if os.path.isfile(args.file[0]) is False:
raise ValueError("File [{}] is not a valid file.".format(args.file[0])) raise ValueError("File [{}] is not a valid file.".format(args.file[0]))
_process_file(args.file[0], template_linters, options, out=sys.stdout) num_violations = _process_file(args.file[0], template_linters, options, out=sys.stdout)
else: else:
if os.path.exists(args.directory[0]) is False or os.path.isfile(args.directory[0]) is True: 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])) raise ValueError("Directory [{}] is not a valid directory.".format(args.directory[0]))
_process_os_walk(args.directory[0], template_linters, options, out=sys.stdout) num_violations = _process_os_walk(args.directory[0], template_linters, options, out=sys.stdout)
if options['is_quiet'] is False:
# matches output of jshint for simplicity
print("")
print("{} violations found".format(num_violations))
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -10,10 +10,50 @@ import textwrap ...@@ -10,10 +10,50 @@ import textwrap
from unittest import TestCase from unittest import TestCase
from ..safe_template_linter import ( from ..safe_template_linter import (
_process_os_walk, FileResults, JavaScriptLinter, MakoTemplateLinter, ParseString, UnderscoreTemplateLinter, Rules _process_os_walk, FileResults, JavaScriptLinter, MakoTemplateLinter, ParseString, StringLines,
UnderscoreTemplateLinter, Rules
) )
@ddt
class TestStringLines(TestCase):
"""
Test StringLines class.
"""
@data(
{'string': 'test', 'index': 0, 'line_start_index': 0, 'line_end_index': 4},
{'string': 'test', 'index': 2, 'line_start_index': 0, 'line_end_index': 4},
{'string': 'test', 'index': 3, 'line_start_index': 0, 'line_end_index': 4},
{'string': '\ntest', 'index': 0, 'line_start_index': 0, 'line_end_index': 1},
{'string': '\ntest', 'index': 2, 'line_start_index': 1, 'line_end_index': 5},
{'string': '\ntest\n', 'index': 0, 'line_start_index': 0, 'line_end_index': 1},
{'string': '\ntest\n', 'index': 2, 'line_start_index': 1, 'line_end_index': 6},
{'string': '\ntest\n', 'index': 6, 'line_start_index': 6, 'line_end_index': 6},
)
def test_string_lines_start_end_index(self, data):
"""
Test StringLines index_to_line_start_index and index_to_line_end_index.
"""
lines = StringLines(data['string'])
self.assertEqual(lines.index_to_line_start_index(data['index']), data['line_start_index'])
self.assertEqual(lines.index_to_line_end_index(data['index']), data['line_end_index'])
@data(
{'string': 'test', 'line_number': 1, 'line': 'test'},
{'string': '\ntest', 'line_number': 1, 'line': ''},
{'string': '\ntest', 'line_number': 2, 'line': 'test'},
{'string': '\ntest\n', 'line_number': 1, 'line': ''},
{'string': '\ntest\n', 'line_number': 2, 'line': 'test'},
{'string': '\ntest\n', 'line_number': 3, 'line': ''},
)
def test_string_lines_start_end_index(self, data):
"""
Test line_number_to_line.
"""
lines = StringLines(data['string'])
self.assertEqual(lines.line_number_to_line(data['line_number']), data['line'])
class TestLinter(TestCase): class TestLinter(TestCase):
""" """
Test Linter base class Test Linter base class
...@@ -31,7 +71,7 @@ class TestSafeTemplateLinter(TestCase): ...@@ -31,7 +71,7 @@ class TestSafeTemplateLinter(TestCase):
Test some top-level linter functions Test some top-level linter functions
""" """
def test_process_os_walk_with_includes(self): def test_process_os_walk(self):
""" """
Tests the top-level processing of template files, including Mako Tests the top-level processing of template files, including Mako
includes. includes.
...@@ -47,9 +87,10 @@ class TestSafeTemplateLinter(TestCase): ...@@ -47,9 +87,10 @@ class TestSafeTemplateLinter(TestCase):
with mock.patch.object(MakoTemplateLinter, '_is_valid_directory', return_value=True): with mock.patch.object(MakoTemplateLinter, '_is_valid_directory', return_value=True):
with mock.patch.object(JavaScriptLinter, '_is_valid_directory', return_value=True): with mock.patch.object(JavaScriptLinter, '_is_valid_directory', return_value=True):
with mock.patch.object(UnderscoreTemplateLinter, '_is_valid_directory', return_value=True): with mock.patch.object(UnderscoreTemplateLinter, '_is_valid_directory', return_value=True):
_process_os_walk('scripts/tests/templates', template_linters, options, out) num_violations = _process_os_walk('scripts/tests/templates', template_linters, options, out)
output = out.getvalue() output = out.getvalue()
self.assertEqual(num_violations, 6)
self.assertIsNotNone(re.search('test\.html.*mako-missing-default', output)) self.assertIsNotNone(re.search('test\.html.*mako-missing-default', output))
self.assertIsNotNone(re.search('test\.coffee.*javascript-concat-html', output)) self.assertIsNotNone(re.search('test\.coffee.*javascript-concat-html', output))
self.assertIsNotNone(re.search('test\.coffee.*underscore-not-escaped', output)) self.assertIsNotNone(re.search('test\.coffee.*underscore-not-escaped', output))
...@@ -83,36 +124,40 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -83,36 +124,40 @@ class TestMakoTemplateLinter(TestLinter):
@data( @data(
{ {
'template': '\n <%page expression_filter="h"/>', 'template': '\n <%page expression_filter="h"/>',
'violations': 0,
'rule': None 'rule': None
}, },
{ {
'template': 'template':
'\n <%page args="section_data" expression_filter="h" /> ', '\n <%page args="section_data" expression_filter="h" /> ',
'violations': 0,
'rule': None 'rule': None
}, },
{ {
'template': '\n ## <%page expression_filter="h"/>',
'rule': Rules.mako_missing_default
},
{
'template': 'template':
'\n <%page expression_filter="h" /> ' '\n <%page expression_filter="h" /> '
'\n <%page args="section_data"/>', '\n <%page args="section_data"/>',
'violations': 1,
'rule': Rules.mako_multiple_page_tags 'rule': Rules.mako_multiple_page_tags
}, },
{ {
'template':
'\n <%page expression_filter="h" /> '
'\n ## <%page args="section_data"/>',
'rule': None
},
{
'template': '\n <%page args="section_data" /> ', 'template': '\n <%page args="section_data" /> ',
'violations': 1,
'rule': Rules.mako_missing_default 'rule': Rules.mako_missing_default
}, },
{ {
'template': 'template':
'\n <%page args="section_data"/> <some-other-tag expression_filter="h" /> ', '\n <%page args="section_data"/> <some-other-tag expression_filter="h" /> ',
'violations': 1,
'rule': Rules.mako_missing_default 'rule': Rules.mako_missing_default
}, },
{ {
'template': '\n', 'template': '\n',
'violations': 1,
'rule': Rules.mako_missing_default 'rule': Rules.mako_missing_default
}, },
) )
...@@ -125,16 +170,18 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -125,16 +170,18 @@ class TestMakoTemplateLinter(TestLinter):
linter._check_mako_file_is_safe(data['template'], results) linter._check_mako_file_is_safe(data['template'], results)
self.assertEqual(len(results.violations), data['violations']) num_violations = 0 if data['rule'] is None else 1
if data['violations'] > 0: self.assertEqual(len(results.violations), num_violations)
if num_violations > 0:
self.assertEqual(results.violations[0].rule, data['rule']) self.assertEqual(results.violations[0].rule, data['rule'])
@data( @data(
{'expression': '${x}', 'rule': None}, {'expression': '${x}', 'rule': None},
{'expression': '${{unbalanced}', 'rule': Rules.mako_unparseable_expression}, {'expression': '${{unbalanced}', 'rule': Rules.mako_unparseable_expression},
{'expression': '${x | n}', 'rule': Rules.mako_invalid_html_filter}, {'expression': '${x | n}', 'rule': Rules.mako_invalid_html_filter},
{'expression': '${x | n, unicode}', 'rule': None}, {'expression': '${x | n, decode.utf8}', 'rule': None},
{'expression': '${x | h}', 'rule': Rules.mako_unwanted_html_filter}, {'expression': '${x | h}', 'rule': Rules.mako_unwanted_html_filter},
{'expression': ' ## ${commented_out | h}', 'rule': None},
{'expression': '${x | n, dump_js_escaped_json}', 'rule': Rules.mako_invalid_html_filter}, {'expression': '${x | n, dump_js_escaped_json}', 'rule': Rules.mako_invalid_html_filter},
) )
def test_check_mako_expressions_in_html(self, data): def test_check_mako_expressions_in_html(self, data):
...@@ -378,7 +425,7 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -378,7 +425,7 @@ class TestMakoTemplateLinter(TestLinter):
{'expression': '${x | n}', 'rule': Rules.mako_invalid_js_filter}, {'expression': '${x | n}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${x | h}', 'rule': Rules.mako_invalid_js_filter}, {'expression': '${x | h}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${x | n, dump_js_escaped_json}', 'rule': None}, {'expression': '${x | n, dump_js_escaped_json}', 'rule': None},
{'expression': '${x | n, unicode}', 'rule': None}, {'expression': '${x | n, decode.utf8}', 'rule': None},
) )
def test_check_mako_expressions_in_javascript(self, data): def test_check_mako_expressions_in_javascript(self, data):
""" """
...@@ -401,7 +448,7 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -401,7 +448,7 @@ class TestMakoTemplateLinter(TestLinter):
@data( @data(
{'expression': '${x}', 'rule': Rules.mako_invalid_js_filter}, {'expression': '${x}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${x | n, js_escaped_string}', 'rule': None}, {'expression': '"${x | n, js_escaped_string}"', 'rule': None},
) )
def test_check_mako_expressions_in_require_js(self, data): def test_check_mako_expressions_in_require_js(self, data):
""" """
...@@ -478,6 +525,63 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -478,6 +525,63 @@ class TestMakoTemplateLinter(TestLinter):
self.assertEqual(results.violations[3].rule, Rules.mako_invalid_js_filter) self.assertEqual(results.violations[3].rule, Rules.mako_invalid_js_filter)
self.assertEqual(results.violations[4].rule, Rules.mako_unwanted_html_filter) self.assertEqual(results.violations[4].rule, Rules.mako_unwanted_html_filter)
def test_check_mako_expressions_javascript_strings(self):
"""
Test _check_mako_file_is_safe javascript string specific rules.
- mako_js_missing_quotes
- mako_js_html_string
"""
linter = MakoTemplateLinter()
results = FileResults('')
mako_template = textwrap.dedent("""
<%page expression_filter="h"/>
<script type="text/javascript">
var valid1 = '${x | n, js_escaped_string} ${y | n, js_escaped_string}'
var valid2 = '${x | n, js_escaped_string} ${y | n, js_escaped_string}'
var valid3 = 'string' + ' ${x | n, js_escaped_string} '
var valid4 = "${Text(_('Some mixed text{begin_span}with html{end_span}')).format(
begin_span=HTML('<span>'),
end_span=HTML('</span>'),
) | n, js_escaped_string}"
var valid5 = " " + "${Text(_('Please {link_start}send us e-mail{link_end}.')).format(
link_start=HTML('<a href="#" id="feedback_email">'),
link_end=HTML('</a>'),
) | n, js_escaped_string}";
var invalid1 = ${x | n, js_escaped_string};
var invalid2 = '<strong>${x | n, js_escaped_string}</strong>'
var invalid3 = '<strong>${x | n, dump_js_escaped_json}</strong>'
</script>
""")
linter._check_mako_file_is_safe(mako_template, results)
self.assertEqual(len(results.violations), 3)
self.assertEqual(results.violations[0].rule, Rules.mako_js_missing_quotes)
self.assertEqual(results.violations[1].rule, Rules.mako_js_html_string)
self.assertEqual(results.violations[2].rule, Rules.mako_js_html_string)
def test_check_javascript_in_mako_javascript_context(self):
"""
Test _check_mako_file_is_safe with JavaScript error in JavaScript
context.
"""
linter = MakoTemplateLinter()
results = FileResults('')
mako_template = textwrap.dedent("""
<%page expression_filter="h"/>
<script type="text/javascript">
var message = '<p>' + msg + '</p>';
</script>
""")
linter._check_mako_file_is_safe(mako_template, results)
self.assertEqual(len(results.violations), 1)
self.assertEqual(results.violations[0].rule, Rules.javascript_concat_html)
self.assertEqual(results.violations[0].start_line, 4)
@data( @data(
{'template': "\n${x | n}", 'parseable': True}, {'template': "\n${x | n}", 'parseable': True},
{ {
...@@ -536,10 +640,10 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -536,10 +640,10 @@ class TestMakoTemplateLinter(TestLinter):
expressions = linter._find_mako_expressions(data['template']) expressions = linter._find_mako_expressions(data['template'])
self.assertEqual(len(expressions), 1) self.assertEqual(len(expressions), 1)
start_index = expressions[0]['start_index'] start_index = expressions[0].start_index
end_index = expressions[0]['end_index'] end_index = expressions[0].end_index
self.assertEqual(data['template'][start_index:end_index], data['template'].strip()) self.assertEqual(data['template'][start_index:end_index], data['template'].strip())
self.assertEqual(expressions[0]['expression'], data['template'].strip()) self.assertEqual(expressions[0].expression, data['template'].strip())
@data( @data(
{'template': " ${{unparseable} ${}", 'start_index': 1}, {'template': " ${{unparseable} ${}", 'start_index': 1},
...@@ -553,8 +657,8 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -553,8 +657,8 @@ class TestMakoTemplateLinter(TestLinter):
expressions = linter._find_mako_expressions(data['template']) expressions = linter._find_mako_expressions(data['template'])
self.assertTrue(2 <= len(expressions)) self.assertTrue(2 <= len(expressions))
self.assertEqual(expressions[0]['start_index'], data['start_index']) self.assertEqual(expressions[0].start_index, data['start_index'])
self.assertIsNone(expressions[0]['expression']) self.assertIsNone(expressions[0].expression)
@data( @data(
{ {
...@@ -577,6 +681,10 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -577,6 +681,10 @@ class TestMakoTemplateLinter(TestLinter):
'template': r""" ${" \" \\"} """, 'template': r""" ${" \" \\"} """,
'result': {'start_index': 3, 'end_index': 11, 'quote_length': 1} 'result': {'start_index': 3, 'end_index': 11, 'quote_length': 1}
}, },
{
'template': "${'broken string}",
'result': {'start_index': 2, 'end_index': None, 'quote_length': None}
},
) )
def test_parse_string(self, data): def test_parse_string(self, data):
""" """
...@@ -592,10 +700,11 @@ class TestMakoTemplateLinter(TestLinter): ...@@ -592,10 +700,11 @@ class TestMakoTemplateLinter(TestLinter):
} }
self.assertDictEqual(string_dict, data['result']) self.assertDictEqual(string_dict, data['result'])
self.assertEqual(data['template'][parse_string.start_index:parse_string.end_index], parse_string.string) if parse_string.end_index is not None:
start_index = parse_string.start_index + parse_string.quote_length self.assertEqual(data['template'][parse_string.start_index:parse_string.end_index], parse_string.string)
end_index = parse_string.end_index - parse_string.quote_length start_inner_index = parse_string.start_index + parse_string.quote_length
self.assertEqual(data['template'][start_index:end_index], parse_string.string_inner) end_inner_index = parse_string.end_index - parse_string.quote_length
self.assertEqual(data['template'][start_inner_index:end_inner_index], parse_string.string_inner)
@ddt @ddt
...@@ -750,21 +859,22 @@ class TestJavaScriptLinter(TestLinter): ...@@ -750,21 +859,22 @@ class TestJavaScriptLinter(TestLinter):
""" """
Test JavaScriptLinter Test JavaScriptLinter
""" """
@data( @data(
{'template': 'var m = "Plain text " + message + "plain text"', 'rule': None}, {'template': 'var m = "Plain text " + message + "plain text"', 'rule': None},
{'template': 'var m = "檌檒濦 " + message + "plain text"', 'rule': None}, {'template': 'var m = "檌檒濦 " + message + "plain text"', 'rule': None},
{'template': 'var m = "<p>" + message + "</p>"', 'rule': Rules.javascript_concat_html}, {'template': 'var m = "<p>" + message + "</p>"', 'rule': Rules.javascript_concat_html},
{'template': ' // var m = "<p>" + commentedOutMessage + "</p>"', 'rule': None},
{'template': 'var m = " <p> " + message + " </p> "', 'rule': Rules.javascript_concat_html}, {'template': 'var m = " <p> " + message + " </p> "', 'rule': Rules.javascript_concat_html},
{'template': 'var m = " <p> " + message + " broken string', 'rule': Rules.javascript_concat_html},
) )
def test_concat_with_html(self, data): def test_concat_with_html(self, data):
""" """
Test _check_javascript_file_is_safe with concatenating strings and HTML Test check_javascript_file_is_safe with concatenating strings and HTML
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
@data( @data(
...@@ -789,12 +899,12 @@ class TestJavaScriptLinter(TestLinter): ...@@ -789,12 +899,12 @@ class TestJavaScriptLinter(TestLinter):
) )
def test_jquery_append(self, data): def test_jquery_append(self, data):
""" """
Test _check_javascript_file_is_safe with JQuery append() Test check_javascript_file_is_safe with JQuery append()
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
...@@ -810,18 +920,19 @@ class TestJavaScriptLinter(TestLinter): ...@@ -810,18 +920,19 @@ class TestJavaScriptLinter(TestLinter):
{'template': 'test.prepend($("<div/>"))', 'rule': None}, {'template': 'test.prepend($("<div/>"))', 'rule': None},
{'template': 'test.prepend(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None}, {'template': 'test.prepend(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
{'template': 'HtmlUtils.prepend($el, someHtml)', 'rule': None}, {'template': 'HtmlUtils.prepend($el, someHtml)', 'rule': None},
{'template': 'test.prepend("broken string)', 'rule': Rules.javascript_jquery_prepend},
{'template': 'test.prepend("fail on concat" + test.render().el)', 'rule': Rules.javascript_jquery_prepend}, {'template': 'test.prepend("fail on concat" + test.render().el)', 'rule': Rules.javascript_jquery_prepend},
{'template': 'test.prepend("fail on concat" + testEl)', 'rule': Rules.javascript_jquery_prepend}, {'template': 'test.prepend("fail on concat" + testEl)', 'rule': Rules.javascript_jquery_prepend},
{'template': 'test.prepend(message)', 'rule': Rules.javascript_jquery_prepend}, {'template': 'test.prepend(message)', 'rule': Rules.javascript_jquery_prepend},
) )
def test_jquery_prepend(self, data): def test_jquery_prepend(self, data):
""" """
Test _check_javascript_file_is_safe with JQuery prepend() Test check_javascript_file_is_safe with JQuery prepend()
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
...@@ -846,14 +957,14 @@ class TestJavaScriptLinter(TestLinter): ...@@ -846,14 +957,14 @@ class TestJavaScriptLinter(TestLinter):
) )
def test_jquery_insertion(self, data): def test_jquery_insertion(self, data):
""" """
Test _check_javascript_file_is_safe with JQuery insertion functions Test check_javascript_file_is_safe with JQuery insertion functions
other than append(), prepend() and html() that take content as an other than append(), prepend() and html() that take content as an
argument (e.g. before(), after()). argument (e.g. before(), after()).
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
...@@ -877,14 +988,14 @@ class TestJavaScriptLinter(TestLinter): ...@@ -877,14 +988,14 @@ class TestJavaScriptLinter(TestLinter):
) )
def test_jquery_insert_to_target(self, data): def test_jquery_insert_to_target(self, data):
""" """
Test _check_javascript_file_is_safe with JQuery insert to target Test check_javascript_file_is_safe with JQuery insert to target
functions that take a target as an argument, like appendTo() and functions that take a target as an argument, like appendTo() and
prependTo(). prependTo().
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
...@@ -897,18 +1008,18 @@ class TestJavaScriptLinter(TestLinter): ...@@ -897,18 +1008,18 @@ class TestJavaScriptLinter(TestLinter):
{'template': 'test.html(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None}, {'template': 'test.html(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
{'template': 'HtmlUtils.setHtml($el, someHtml)', 'rule': None}, {'template': 'HtmlUtils.setHtml($el, someHtml)', 'rule': None},
{'template': 'test.html("any string")', 'rule': Rules.javascript_jquery_html}, {'template': 'test.html("any string")', 'rule': Rules.javascript_jquery_html},
{'template': 'test.html("broken string)', 'rule': Rules.javascript_jquery_html},
{'template': 'test.html("檌檒濦")', 'rule': Rules.javascript_jquery_html}, {'template': 'test.html("檌檒濦")', 'rule': Rules.javascript_jquery_html},
{'template': 'test.html(anything)', 'rule': Rules.javascript_jquery_html}, {'template': 'test.html(anything)', 'rule': Rules.javascript_jquery_html},
) )
def test_jquery_html(self, data): def test_jquery_html(self, data):
""" """
Test _check_javascript_file_is_safe with JQuery html() Test check_javascript_file_is_safe with JQuery html()
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
@data( @data(
...@@ -918,26 +1029,26 @@ class TestJavaScriptLinter(TestLinter): ...@@ -918,26 +1029,26 @@ class TestJavaScriptLinter(TestLinter):
) )
def test_javascript_interpolate(self, data): def test_javascript_interpolate(self, data):
""" """
Test _check_javascript_file_is_safe with interpolate() Test check_javascript_file_is_safe with interpolate()
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
@data( @data(
{'template': '_.escape()', 'rule': None}, {'template': '_.escape(message)', 'rule': None},
{'template': 'anything.escape()', 'rule': Rules.javascript_escape}, {'template': 'anything.escape(message)', 'rule': Rules.javascript_escape},
) )
def test_javascript_interpolate(self, data): def test_javascript_interpolate(self, data):
""" """
Test _check_javascript_file_is_safe with interpolate() Test check_javascript_file_is_safe with interpolate()
""" """
linter = JavaScriptLinter() linter = JavaScriptLinter()
results = FileResults('') results = FileResults('')
linter._check_javascript_file_is_safe(data['template'], results) linter.check_javascript_file_is_safe(data['template'], results)
self._validate_data_rule(data, results) self._validate_data_rule(data, results)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment