diff --git a/InvenTree/part/templatetags/js_i18n.py b/InvenTree/part/templatetags/js_i18n.py new file mode 100644 index 0000000000..f9fc69fdf1 --- /dev/null +++ b/InvenTree/part/templatetags/js_i18n.py @@ -0,0 +1,102 @@ +"""This module provides custom translation tags specifically for use with javascript code. + +Translated strings are escaped, such that they can be used as string literals in a javascript file. +""" + +from django import template +from django.template import TemplateSyntaxError +from django.templatetags.i18n import TranslateNode + +import bleach + +register = template.Library() + + +class CustomTranslateNode(TranslateNode): + """Custom translation node class, which sanitizes the translated strings for javascript use""" + + def render(self, context): + """Custom render function overrides / extends default behaviour""" + + result = super().render(context) + + result = bleach.clean(result) + + # Remove any escape sequences + for seq in ['\a', '\b', '\f', '\n', '\r', '\t', '\v']: + result = result.replace(seq, '') + + # Remove other disallowed characters + for c in ['\\', '`', ';', '|', '&']: + result = result.replace(c, '') + + # Escape any quotes contained in the string + result = result.replace("'", r"\'") + result = result.replace('"', r'\"') + + # Return the 'clean' resulting string + return result + + +@register.tag("translate") +@register.tag("trans") +def do_translate(parser, token): + """Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py + + The only difference is that we pass this to our custom rendering node class + """ + + bits = token.split_contents() + if len(bits) < 2: + raise TemplateSyntaxError("'%s' takes at least one argument" % bits[0]) + message_string = parser.compile_filter(bits[1]) + remaining = bits[2:] + + noop = False + asvar = None + message_context = None + seen = set() + invalid_context = {"as", "noop"} + + while remaining: + option = remaining.pop(0) + if option in seen: + raise TemplateSyntaxError( + "The '%s' option was specified more than once." % option, + ) + elif option == "noop": + noop = True + elif option == "context": + try: + value = remaining.pop(0) + except IndexError: + raise TemplateSyntaxError( + "No argument provided to the '%s' tag for the context option." + % bits[0] + ) + if value in invalid_context: + raise TemplateSyntaxError( + "Invalid argument '%s' provided to the '%s' tag for the context " + "option" % (value, bits[0]), + ) + message_context = parser.compile_filter(value) + elif option == "as": + try: + value = remaining.pop(0) + except IndexError: + raise TemplateSyntaxError( + "No argument provided to the '%s' tag for the as option." % bits[0] + ) + asvar = value + else: + raise TemplateSyntaxError( + "Unknown argument for '%s' tag: '%s'. The only options " + "available are 'noop', 'context' \"xxx\", and 'as VAR'." + % ( + bits[0], + option, + ) + ) + seen.add(option) + + return CustomTranslateNode(message_string, noop, asvar, message_context)