diff --git a/src/django_nh3/templatetags/__init__.py b/src/django_nh3/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/django_nh3/templatetags/nh3_tags.py b/src/django_nh3/templatetags/nh3_tags.py new file mode 100644 index 0000000..71c3d85 --- /dev/null +++ b/src/django_nh3/templatetags/nh3_tags.py @@ -0,0 +1,32 @@ +import nh3 +from django import template +from django.utils.safestring import SafeText, mark_safe + +from django_nh3.utils import get_nh3_default_options + +register = template.Library() + + +@register.filter(name="nh3") +def nh3_value(value: str | None, tags: str | None = None) -> SafeText: + """ + Takes an input HTML value and sanitizes it utilizing nh3, + returning a SafeText object that can be rendered by Django. + + Accepts an optional argument of allowed tags. Should be a comma delimited + string (ie. "img,span" or "img") + """ + if value is None: + return None + + args = {} + + nh3_args = get_nh3_default_options() + if tags is not None: + args = nh3_args.copy() + args["tags"] = set(tags.split(",")) + else: + args = nh3_args + + nh3_value = nh3.clean(value, **args) + return mark_safe(nh3_value) diff --git a/src/django_nh3/utils.py b/src/django_nh3/utils.py new file mode 100644 index 0000000..9d8ffbd --- /dev/null +++ b/src/django_nh3/utils.py @@ -0,0 +1,50 @@ +import logging +from typing import Any + +from django.conf import settings + +logger = logging.getLogger(__name__) + + +def get_nh3_default_options() -> dict[str, Any]: + """ + Pull the django-nh3 settings similarly to how django-bleach handled them. + + Some django-bleach settings can be mapped to django-nh3 settings without + any changes: + + BLEACH_ALLOWED_TAGS -> NH3_ALLOWED_TAGS + BLEACH_ALLOWED_ATTRIBUTES -> NH3_ALLOWED_ATTRIBUTES + BLEACH_STRIP_COMMENTS -> NH3_STRIP_COMMENTS + + While other settings are have no current support in nh3: + + BLEACH_ALLOWED_STYLES -> There is no support for styling + BLEACH_ALLOWED_PROTOCOLS -> There is no suport for protocols + BLEACH_STRIP_TAGS -> This is the default behavior of nh3 + + """ + nh3_args: dict[str, Any] = {} + + nh3_settings = { + "NH3_ALLOWED_TAGS": "tags", + "NH3_ALLOWED_ATTRIBUTES": "attributes", + "NH3_STRIP_COMMENTS": "strip_comments", + } + + for setting, kwarg in nh3_settings.items(): + if hasattr(settings, setting): + attr = getattr(settings, setting) + + # Convert from general iterables to sets + if setting == "NH3_ALLOWED_TAGS": + attr = set(attr) + elif setting == "NH3_ALLOWED_ATTRIBUTES": + copy_dict = attr.copy() + for tag, attributes in attr.items(): + copy_dict[tag] = set(attributes) + attr = copy_dict + + nh3_args[kwarg] = attr + + return nh3_args diff --git a/tests/constants.py b/tests/constants.py new file mode 100644 index 0000000..d21e56e --- /dev/null +++ b/tests/constants.py @@ -0,0 +1,5 @@ +ALLOWED_ATTRIBUTES = {"*": {"class", "style"}, "a": {"href", "title"}} + +ALLOWED_TAGS = {"a", "li", "ul"} + +STRIP_COMMENTS = True diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..ab0581b --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,32 @@ +from unittest.mock import patch + +from django.test import TestCase +from django_nh3.utils import get_nh3_default_options + +from .constants import ALLOWED_ATTRIBUTES, ALLOWED_TAGS, STRIP_COMMENTS + + +class TestBleachOptions(TestCase): + @patch( + "django_nh3.utils.settings", + NH3_ALLOWED_ATTRIBUTES=ALLOWED_ATTRIBUTES, + ) + def test_custom_attrs(self, settings): + nh3_args = get_nh3_default_options() + self.assertEqual(nh3_args["attributes"], ALLOWED_ATTRIBUTES) + + @patch( + "django_nh3.utils.settings", + NH3_ALLOWED_TAGS=ALLOWED_TAGS, + ) + def test_custom_tags(self, settings): + nh3_args = get_nh3_default_options() + self.assertEqual(nh3_args["tags"], ALLOWED_TAGS) + + @patch( + "django_nh3.utils.settings", + NH3_STRIP_COMMENTS=STRIP_COMMENTS, + ) + def test_strip_comments(self, settings): + nh3_args = get_nh3_default_options() + self.assertEqual(nh3_args["strip_comments"], STRIP_COMMENTS) diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py new file mode 100644 index 0000000..9d86d2d --- /dev/null +++ b/tests/test_templatetags.py @@ -0,0 +1,45 @@ +from django.template import Context, Template +from django.test import TestCase + + +class TestBleachTemplates(TestCase): + """Test template tags""" + + def test_bleaching(self): + """Test that unsafe tags are sanitised""" + context = Context( + {"some_unsafe_content": ''}, + ) + template_to_render = Template( + "{% load nh3_tags %}" "{{ some_unsafe_content|nh3 }}" + ) + rendered_template = template_to_render.render(context) + self.assertEqual("", rendered_template) + + def test_bleaching_none(self): + """Test that None is handled properly as an input""" + context = Context({"none_value": None}) + template_to_render = Template( + "{% load nh3_tags %}" "{{ none_value|nh3 }}" + ) + rendered_template = template_to_render.render(context) + self.assertEqual("None", rendered_template) + + def test_bleaching_tags(self): + """Test provided tags are kept""" + context = Context( + { + "some_unsafe_content": ( + "" + "I'm not trying to XSS you" + ) + } + ) + template_to_render = Template( + "{% load nh3_tags %}" '{{ some_unsafe_content|nh3:"img" }}' + ) + rendered_template = template_to_render.render(context) + self.assertInHTML( + 'I\'m not trying to XSS you', rendered_template + )