django-safe-template-engine 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. django_safe_template_engine-1.0.0/.flake8 +7 -0
  2. django_safe_template_engine-1.0.0/.github/workflows/build.yml +30 -0
  3. django_safe_template_engine-1.0.0/.gitignore +135 -0
  4. django_safe_template_engine-1.0.0/LICENSE +28 -0
  5. django_safe_template_engine-1.0.0/PKG-INFO +166 -0
  6. django_safe_template_engine-1.0.0/README.md +118 -0
  7. django_safe_template_engine-1.0.0/isort.cfg +2 -0
  8. django_safe_template_engine-1.0.0/mypy.ini +8 -0
  9. django_safe_template_engine-1.0.0/pyproject.toml +27 -0
  10. django_safe_template_engine-1.0.0/run_tests.py +7 -0
  11. django_safe_template_engine-1.0.0/src/django_safe_template_engine/__init__.py +0 -0
  12. django_safe_template_engine-1.0.0/src/django_safe_template_engine/engine.py +16 -0
  13. django_safe_template_engine-1.0.0/src/django_safe_template_engine/trusted_filters.py +72 -0
  14. django_safe_template_engine-1.0.0/src/django_safe_template_engine/trusted_tags.py +27 -0
  15. django_safe_template_engine-1.0.0/src/django_safe_template_engine/validators.py +11 -0
  16. django_safe_template_engine-1.0.0/tests/__init__.py +0 -0
  17. django_safe_template_engine-1.0.0/tests/filters/__init__.py +0 -0
  18. django_safe_template_engine-1.0.0/tests/filters/test_trusted_filters.py +172 -0
  19. django_safe_template_engine-1.0.0/tests/filters/test_untrusted_filters.py +27 -0
  20. django_safe_template_engine-1.0.0/tests/loaders/__init__.py +0 -0
  21. django_safe_template_engine-1.0.0/tests/loaders/test_untrusted_loaders.py +45 -0
  22. django_safe_template_engine-1.0.0/tests/requirements.txt +11 -0
  23. django_safe_template_engine-1.0.0/tests/settings.py +1 -0
  24. django_safe_template_engine-1.0.0/tests/tags/__init__.py +0 -0
  25. django_safe_template_engine-1.0.0/tests/tags/test_trusted_tags.py +211 -0
  26. django_safe_template_engine-1.0.0/tests/tags/test_untrusted_tags.py +59 -0
  27. django_safe_template_engine-1.0.0/tests/test_validators.py +39 -0
@@ -0,0 +1,7 @@
1
+ [flake8]
2
+ max-line-length = 119
3
+ exclude =
4
+ .venv
5
+ ignore =
6
+ # See https://www.flake8rules.com/rules/W503.html
7
+ W503
@@ -0,0 +1,30 @@
1
+ name: build
2
+ run-name: Workflow triggered by ${{ github.actor }}
3
+
4
+ on: [push]
5
+
6
+ jobs:
7
+ run_tests:
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - name: Set up Python ${{ matrix.python-version }}
16
+ uses: actions/setup-python@v4
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install dependencies
20
+ run: |
21
+ cd tests/
22
+ python -m pip install -r requirements.txt
23
+ - name: Check Python typing
24
+ run: mypy --config-file mypy.ini src tests
25
+ - name: Validate ordering of Python imports with isort
26
+ run: isort --settings-path isort.cfg --check-only src tests
27
+ - name: Validate Python syntax with flake8
28
+ run: flake8 --config .flake8 src tests
29
+ - name: Run tests
30
+ run: python run_tests.py
@@ -0,0 +1,135 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # macOS
132
+ .DS_Store
133
+
134
+ # Editor and IDE files
135
+ .vscode
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, GuestReady
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.3
2
+ Name: django_safe_template_engine
3
+ Version: 1.0.0
4
+ Summary: A Django template engine to render untrusted template code
5
+ Project-URL: Homepage, https://github.com/ronanboiteau/django_safe_template_engine
6
+ Project-URL: Documentation, https://github.com/ronanboiteau/django_safe_template_engine/blob/main/README.md
7
+ Project-URL: Repository, https://github.com/ronanboiteau/django_safe_template_engine.git
8
+ Project-URL: Issues, https://github.com/ronanboiteau/django_safe_template_engine/issues
9
+ Project-URL: Changelog, https://github.com/ronanboiteau/django_safe_template_engine/blob/main/CHANGELOG.md
10
+ Author-email: Ronan Boiteau <ronan@boiteau.eu>
11
+ Maintainer-email: Ronan Boiteau <ronan@boiteau.eu>
12
+ License: BSD 3-Clause License
13
+
14
+ Copyright (c) 2023, GuestReady
15
+
16
+ Redistribution and use in source and binary forms, with or without
17
+ modification, are permitted provided that the following conditions are met:
18
+
19
+ 1. Redistributions of source code must retain the above copyright notice, this
20
+ list of conditions and the following disclaimer.
21
+
22
+ 2. Redistributions in binary form must reproduce the above copyright notice,
23
+ this list of conditions and the following disclaimer in the documentation
24
+ and/or other materials provided with the distribution.
25
+
26
+ 3. Neither the name of the copyright holder nor the names of its
27
+ contributors may be used to endorse or promote products derived from
28
+ this software without specific prior written permission.
29
+
30
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
31
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
32
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
33
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
34
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
35
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
36
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
37
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
38
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40
+ License-File: LICENSE
41
+ Keywords: django,engine,safe,template
42
+ Classifier: License :: OSI Approved :: BSD License
43
+ Classifier: Operating System :: OS Independent
44
+ Classifier: Programming Language :: Python :: 3
45
+ Requires-Python: >=3.8
46
+ Requires-Dist: django<3.1,>=3
47
+ Description-Content-Type: text/markdown
48
+
49
+ ![GitHub Actions build status](https://github.com/guestready/django_safe_template_engine/actions/workflows/build.yml/badge.svg?branch=main)
50
+
51
+ # Django Safe Template Engine
52
+
53
+ Django template engine to render untrusted template code
54
+
55
+ ## Requirements
56
+
57
+ - Python 3.8 to 3.11
58
+ - Django 3.0
59
+
60
+ ## Available tools
61
+
62
+ Template engine:
63
+
64
+ ```py
65
+ from django.template import Template
66
+ from django_safe_template_engine.engine import SafeTemplateEngine
67
+
68
+ safe_engine = SafeTemplateEngine()
69
+ Template(source, engine=safe_engine)
70
+ ```
71
+
72
+ Validator:
73
+
74
+ ```py
75
+ from django_safe_template_engine.validators import validate_safe_engine_template_syntax
76
+
77
+ template_code = '{% include "hacked.html" %}'
78
+ validate_safe_engine_template_syntax(template_code)
79
+ ```
80
+
81
+ ## Trusted built-ins
82
+
83
+ The following tags and filters are allowed by this template engine.
84
+
85
+ ### Trusted tags
86
+
87
+ - [`autoescape`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#autoescape)
88
+ - [`comment`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#comment)
89
+ - [`cycle`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#cycle)
90
+ - [`filter`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#filter)
91
+ - [`firstof`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#firstof)
92
+ - [`for`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#for)
93
+ - [`for … empty`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#for-empty)
94
+ - [`if`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#if)
95
+ - [`ifchanged`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#ifchanged)
96
+ - [`lorem`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#lorem)
97
+ - [`now`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#now)
98
+ - [`regroup`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#regroup)
99
+ - [`resetcycle`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#resetcycle)
100
+ - [`spaceless`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#spaceless)
101
+ - [`templatetag`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#templatetag)
102
+ - [`url`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#url)
103
+ - [`verbatim`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#verbatim)
104
+ - [`widthratio`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#widthratio)
105
+ - [`with`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#with)
106
+
107
+ ### Trusted filters
108
+
109
+ <!-- TODO: Check for dead links -->
110
+ <!-- TODO: Re-order? -->
111
+ - [`addslashes`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#addslashes)
112
+ - [`capfirst`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#capfirst)
113
+ - [`escapejs`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#escapejs)
114
+ - [`json_script`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#json_script)
115
+ - [`floatformat`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#floatformat)
116
+ - [`iriencode`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#iriencode)
117
+ - [`linenumbers`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#linenumbers)
118
+ - [`lower`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#lower)
119
+ - [`make_list`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#make_list)
120
+ - [`slugify`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#slugify)
121
+ - [`stringformat`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#stringformat)
122
+ - [`title`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#title)
123
+ - [`truncatechars`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatechars)
124
+ - [`truncatechars_html`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatechars_html)
125
+ - [`truncatewords`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatewords)
126
+ - [`truncatewords_html`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatewords_html)
127
+ - [`upper`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#upper)
128
+ - [`urlencode`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#urlencode)
129
+ - [`urlize`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#urlize)
130
+ - [`urlizetrunc`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#urlizetrunc)
131
+ - [`wordcount`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#wordcount)
132
+ - [`wordwrap`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#wordwrap)
133
+ - [`ljust`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#ljust)
134
+ - [`rjust`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#rjust)
135
+ - [`center`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#center)
136
+ - [`cut`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#cut)
137
+ - [`escape`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#escape)
138
+ - [`force_escape`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#force_escape)
139
+ - [`linebreaks`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#linebreaks)
140
+ - [`linebreaksbr`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#linebreaksbr)
141
+ - [`safe`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#safe)
142
+ - [`safeseq`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#safeseq)
143
+ - [`striptags`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#striptags)
144
+ - [`dictsort`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#dictsort)
145
+ - [`dictsortreversed`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#dictsortreversed)
146
+ - [`first`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#first)
147
+ - [`join`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#join)
148
+ - [`last`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#last)
149
+ - [`length`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#length)
150
+ - [`length_is`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#length_is)
151
+ - [`random`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#random)
152
+ - [`slice`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#slice)
153
+ - [`unordered_list`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#unordered_list)
154
+ - [`add`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#add)
155
+ - [`get_digit`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#get_digit)
156
+ - [`date`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date)
157
+ - [`time`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#time)
158
+ - [`timesince`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#timesince)
159
+ - [`timeuntil`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#timeuntil)
160
+ - [`default`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#default)
161
+ - [`default_if_none`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#default_if_none)
162
+ - [`divisibleby`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#divisibleby)
163
+ - [`yesno`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#yesno)
164
+ - [`filesizeformat`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#filesizeformat)
165
+ - [`pluralize`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#pluralize)
166
+ - [`phone2numeric`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#phone2numeric)
@@ -0,0 +1,118 @@
1
+ ![GitHub Actions build status](https://github.com/guestready/django_safe_template_engine/actions/workflows/build.yml/badge.svg?branch=main)
2
+
3
+ # Django Safe Template Engine
4
+
5
+ Django template engine to render untrusted template code
6
+
7
+ ## Requirements
8
+
9
+ - Python 3.8 to 3.11
10
+ - Django 3.0
11
+
12
+ ## Available tools
13
+
14
+ Template engine:
15
+
16
+ ```py
17
+ from django.template import Template
18
+ from django_safe_template_engine.engine import SafeTemplateEngine
19
+
20
+ safe_engine = SafeTemplateEngine()
21
+ Template(source, engine=safe_engine)
22
+ ```
23
+
24
+ Validator:
25
+
26
+ ```py
27
+ from django_safe_template_engine.validators import validate_safe_engine_template_syntax
28
+
29
+ template_code = '{% include "hacked.html" %}'
30
+ validate_safe_engine_template_syntax(template_code)
31
+ ```
32
+
33
+ ## Trusted built-ins
34
+
35
+ The following tags and filters are allowed by this template engine.
36
+
37
+ ### Trusted tags
38
+
39
+ - [`autoescape`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#autoescape)
40
+ - [`comment`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#comment)
41
+ - [`cycle`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#cycle)
42
+ - [`filter`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#filter)
43
+ - [`firstof`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#firstof)
44
+ - [`for`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#for)
45
+ - [`for … empty`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#for-empty)
46
+ - [`if`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#if)
47
+ - [`ifchanged`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#ifchanged)
48
+ - [`lorem`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#lorem)
49
+ - [`now`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#now)
50
+ - [`regroup`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#regroup)
51
+ - [`resetcycle`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#resetcycle)
52
+ - [`spaceless`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#spaceless)
53
+ - [`templatetag`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#templatetag)
54
+ - [`url`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#url)
55
+ - [`verbatim`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#verbatim)
56
+ - [`widthratio`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#widthratio)
57
+ - [`with`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#with)
58
+
59
+ ### Trusted filters
60
+
61
+ <!-- TODO: Check for dead links -->
62
+ <!-- TODO: Re-order? -->
63
+ - [`addslashes`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#addslashes)
64
+ - [`capfirst`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#capfirst)
65
+ - [`escapejs`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#escapejs)
66
+ - [`json_script`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#json_script)
67
+ - [`floatformat`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#floatformat)
68
+ - [`iriencode`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#iriencode)
69
+ - [`linenumbers`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#linenumbers)
70
+ - [`lower`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#lower)
71
+ - [`make_list`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#make_list)
72
+ - [`slugify`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#slugify)
73
+ - [`stringformat`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#stringformat)
74
+ - [`title`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#title)
75
+ - [`truncatechars`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatechars)
76
+ - [`truncatechars_html`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatechars_html)
77
+ - [`truncatewords`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatewords)
78
+ - [`truncatewords_html`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#truncatewords_html)
79
+ - [`upper`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#upper)
80
+ - [`urlencode`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#urlencode)
81
+ - [`urlize`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#urlize)
82
+ - [`urlizetrunc`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#urlizetrunc)
83
+ - [`wordcount`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#wordcount)
84
+ - [`wordwrap`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#wordwrap)
85
+ - [`ljust`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#ljust)
86
+ - [`rjust`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#rjust)
87
+ - [`center`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#center)
88
+ - [`cut`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#cut)
89
+ - [`escape`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#escape)
90
+ - [`force_escape`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#force_escape)
91
+ - [`linebreaks`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#linebreaks)
92
+ - [`linebreaksbr`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#linebreaksbr)
93
+ - [`safe`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#safe)
94
+ - [`safeseq`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#safeseq)
95
+ - [`striptags`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#striptags)
96
+ - [`dictsort`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#dictsort)
97
+ - [`dictsortreversed`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#dictsortreversed)
98
+ - [`first`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#first)
99
+ - [`join`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#join)
100
+ - [`last`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#last)
101
+ - [`length`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#length)
102
+ - [`length_is`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#length_is)
103
+ - [`random`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#random)
104
+ - [`slice`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#slice)
105
+ - [`unordered_list`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#unordered_list)
106
+ - [`add`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#add)
107
+ - [`get_digit`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#get_digit)
108
+ - [`date`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#date)
109
+ - [`time`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#time)
110
+ - [`timesince`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#timesince)
111
+ - [`timeuntil`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#timeuntil)
112
+ - [`default`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#default)
113
+ - [`default_if_none`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#default_if_none)
114
+ - [`divisibleby`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#divisibleby)
115
+ - [`yesno`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#yesno)
116
+ - [`filesizeformat`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#filesizeformat)
117
+ - [`pluralize`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#pluralize)
118
+ - [`phone2numeric`](https://docs.djangoproject.com/en/dev/ref/templates/builtins/#phone2numeric)
@@ -0,0 +1,2 @@
1
+ [isort]
2
+ profile=django
@@ -0,0 +1,8 @@
1
+ [mypy]
2
+ python_version = 3.9
3
+ show_error_codes = True
4
+ explicit_package_bases = true
5
+ namespace_packages = true
6
+
7
+ # Ignore
8
+ ignore_missing_imports = true
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "django_safe_template_engine"
3
+ version = "1.0.0"
4
+ dependencies = ["django >= 3, < 3.1"]
5
+ requires-python = ">=3.8"
6
+ authors = [{ name = "Ronan Boiteau", email = "ronan@boiteau.eu" }]
7
+ maintainers = [{ name = "Ronan Boiteau", email = "ronan@boiteau.eu" }]
8
+ description = "A Django template engine to render untrusted template code"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ keywords = ["django", "safe", "template", "engine"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: BSD License",
15
+ "Operating System :: OS Independent",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/ronanboiteau/django_safe_template_engine"
24
+ Documentation = "https://github.com/ronanboiteau/django_safe_template_engine/blob/main/README.md"
25
+ Repository = "https://github.com/ronanboiteau/django_safe_template_engine.git"
26
+ Issues = "https://github.com/ronanboiteau/django_safe_template_engine/issues"
27
+ Changelog = "https://github.com/ronanboiteau/django_safe_template_engine/blob/main/CHANGELOG.md"
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ import pytest
4
+
5
+ if __name__ == '__main__':
6
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings')
7
+ pytest.main()
@@ -0,0 +1,16 @@
1
+ from django.template.engine import Engine
2
+
3
+
4
+ class SafeTemplateEngine(Engine):
5
+
6
+ def __init__(self, **kwargs):
7
+ self.default_builtins = [
8
+ 'django_safe_template_engine.trusted_filters',
9
+ 'django_safe_template_engine.trusted_tags',
10
+ ]
11
+
12
+ super().__init__(**kwargs)
13
+
14
+ self.dirs = []
15
+ self.app_dirs = False
16
+ self.loaders = []
@@ -0,0 +1,72 @@
1
+ from django.template.defaultfilters import (
2
+ add, addslashes, capfirst, center, cut, date, default, default_if_none,
3
+ dictsort, dictsortreversed, divisibleby, escape_filter, escapejs_filter,
4
+ filesizeformat, first, floatformat, force_escape, get_digit, iriencode,
5
+ join, json_script, last, length, length_is, linebreaks_filter,
6
+ linebreaksbr, linenumbers, ljust, lower, make_list, phone2numeric_filter,
7
+ pluralize, random, rjust, safe, safeseq, slice_filter, slugify,
8
+ stringformat, striptags, time, timesince_filter, timeuntil_filter, title,
9
+ truncatechars, truncatechars_html, truncatewords, truncatewords_html,
10
+ unordered_list, upper, urlencode, urlize, urlizetrunc, wordcount, wordwrap,
11
+ yesno,
12
+ )
13
+ from django.template.library import Library
14
+
15
+ register = Library()
16
+
17
+ register.filter(addslashes, is_safe=True)
18
+ register.filter(capfirst, is_safe=True)
19
+ register.filter('escapejs', escapejs_filter)
20
+ register.filter(json_script, is_safe=True)
21
+ register.filter(floatformat, is_safe=True)
22
+ register.filter(iriencode, is_safe=True)
23
+ register.filter(linenumbers, is_safe=True, needs_autoescape=True)
24
+ register.filter(lower, is_safe=True)
25
+ register.filter(make_list, is_safe=False)
26
+ register.filter(slugify, is_safe=True)
27
+ register.filter(stringformat, is_safe=True)
28
+ register.filter(title, is_safe=True)
29
+ register.filter(truncatechars, is_safe=True)
30
+ register.filter(truncatechars_html, is_safe=True)
31
+ register.filter(truncatewords, is_safe=True)
32
+ register.filter(truncatewords_html, is_safe=True)
33
+ register.filter(upper, is_safe=False)
34
+ register.filter(urlencode, is_safe=False)
35
+ register.filter(urlize, is_safe=True, needs_autoescape=True)
36
+ register.filter(urlizetrunc, is_safe=True, needs_autoescape=True)
37
+ register.filter(wordcount, is_safe=False)
38
+ register.filter(wordwrap, is_safe=True)
39
+ register.filter(ljust, is_safe=True)
40
+ register.filter(rjust, is_safe=True)
41
+ register.filter(center, is_safe=True)
42
+ register.filter(cut)
43
+ register.filter('escape', escape_filter, is_safe=True)
44
+ register.filter(force_escape, is_safe=True)
45
+ register.filter('linebreaks', linebreaks_filter, is_safe=True, needs_autoescape=True)
46
+ register.filter(linebreaksbr, is_safe=True, needs_autoescape=True)
47
+ register.filter(safe, is_safe=True)
48
+ register.filter(safeseq, is_safe=True)
49
+ register.filter(striptags, is_safe=True)
50
+ register.filter(dictsort, is_safe=False)
51
+ register.filter(dictsortreversed, is_safe=False)
52
+ register.filter(first, is_safe=False)
53
+ register.filter(join, is_safe=True, needs_autoescape=True)
54
+ register.filter(last, is_safe=True)
55
+ register.filter(length, is_safe=False)
56
+ register.filter(length_is, is_safe=False)
57
+ register.filter(random, is_safe=True)
58
+ register.filter('slice', slice_filter, is_safe=True)
59
+ register.filter(unordered_list, is_safe=True, needs_autoescape=True)
60
+ register.filter(add, is_safe=False)
61
+ register.filter(get_digit, is_safe=False)
62
+ register.filter(date, expects_localtime=True, is_safe=False)
63
+ register.filter(time, expects_localtime=True, is_safe=False)
64
+ register.filter('timesince', timesince_filter, is_safe=False)
65
+ register.filter('timeuntil', timeuntil_filter, is_safe=False)
66
+ register.filter(default, is_safe=False)
67
+ register.filter(default_if_none, is_safe=False)
68
+ register.filter(divisibleby, is_safe=False)
69
+ register.filter(yesno, is_safe=False)
70
+ register.filter(filesizeformat, is_safe=True)
71
+ register.filter(pluralize, is_safe=False)
72
+ register.filter('phone2numeric', phone2numeric_filter, is_safe=True)
@@ -0,0 +1,27 @@
1
+ from django.template.defaulttags import (
2
+ autoescape, comment, cycle, do_filter, do_for, do_if, do_with, firstof,
3
+ ifchanged, lorem, now, regroup, resetcycle, spaceless, templatetag, url,
4
+ verbatim, widthratio,
5
+ )
6
+ from django.template.library import Library
7
+
8
+ register = Library()
9
+
10
+ register.tag(autoescape)
11
+ register.tag(comment)
12
+ register.tag(cycle)
13
+ register.tag('filter', do_filter)
14
+ register.tag(firstof)
15
+ register.tag('for', do_for)
16
+ register.tag('if', do_if)
17
+ register.tag(ifchanged)
18
+ register.tag(lorem)
19
+ register.tag(now)
20
+ register.tag(regroup)
21
+ register.tag(resetcycle)
22
+ register.tag(spaceless)
23
+ register.tag(templatetag)
24
+ register.tag(url)
25
+ register.tag(verbatim)
26
+ register.tag(widthratio)
27
+ register.tag('with', do_with)
@@ -0,0 +1,11 @@
1
+ from django.core.exceptions import ValidationError
2
+ from django.template import Template, TemplateSyntaxError
3
+
4
+ from django_safe_template_engine.engine import SafeTemplateEngine
5
+
6
+
7
+ def validate_safe_engine_template_syntax(source):
8
+ try:
9
+ Template(source, engine=SafeTemplateEngine())
10
+ except (TemplateSyntaxError) as err:
11
+ raise ValidationError(str(err))
File without changes
@@ -0,0 +1,172 @@
1
+ from django.template.base import Template
2
+ from django.template.context import Context
3
+
4
+ from django_safe_template_engine.engine import SafeTemplateEngine
5
+
6
+
7
+ class TestTrustedFilters:
8
+ engine = SafeTemplateEngine()
9
+
10
+ def _render(self, template_code, context={}):
11
+ template = Template(template_code, engine=self.engine)
12
+ return template.render(Context(context))
13
+
14
+ def test_trust_addslashes(self):
15
+ expected = '\\\\test'
16
+ result = self._render('{{ "\\test"|addslashes }}')
17
+ assert result == expected
18
+
19
+ def test_trust_capfist(self):
20
+ expected = 'Hello world'
21
+ result = self._render('{{ "hello world"|capfirst }}')
22
+ assert result == expected
23
+
24
+ def test_trust_escapejs(self):
25
+ expected = '\\u003Ctest\\u0026test\\u003E'
26
+ result = self._render('{{ "<test&test>"|escapejs }}')
27
+ assert result == expected
28
+
29
+ def test_trust_json_script(self):
30
+ expected = '<script id="test-id" type="application/json">"{\\"id\\": 1}"</script>'
31
+ result = self._render(
32
+ '{{ json_string|json_script:"test-id" }}',
33
+ context={'json_string': '{"id": 1}'}
34
+ )
35
+ assert result == expected
36
+
37
+ def test_trust_floatformat(self):
38
+ expected = '42'
39
+ result = self._render('{{ 42.0000|floatformat }}')
40
+ assert result == expected
41
+
42
+ # TODO: Write test for iriencode
43
+
44
+ def test_trust_linenumbers(self):
45
+ expected = '1. one\n2. two\n3. three'
46
+ result = self._render(
47
+ '{{ value|linenumbers }}',
48
+ context={'value': 'one\ntwo\nthree'},
49
+ )
50
+ assert result == expected
51
+
52
+ def test_trust_lower(self):
53
+ expected = 'hello'
54
+ result = self._render('{{ "HeLlO"|lower }}')
55
+ assert result == expected
56
+
57
+ def test_trust_make_list(self):
58
+ expected = "[&#x27;1&#x27;, &#x27;2&#x27;, &#x27;3&#x27;]"
59
+ result = self._render(
60
+ '{{ value|make_list }}',
61
+ context={'value': '123'},
62
+ )
63
+ assert result == expected
64
+
65
+ def test_trust_slugify(self):
66
+ expected = 'hello-world'
67
+ result = self._render('{{ "hello world"|slugify }}')
68
+ assert result == expected
69
+
70
+ # TODO: Write test for stringformat
71
+
72
+ def test_trust_title(self):
73
+ expected = 'Hello World'
74
+ result = self._render('{{ "hello world"|title }}')
75
+ assert result == expected
76
+
77
+ # TODO: Write test for truncatechars
78
+ # TODO: Write test for truncatechars_html
79
+ # TODO: Write test for truncatewords
80
+ # TODO: Write test for truncatewords_html
81
+
82
+ def test_trust_upper(self):
83
+ expected = 'HELLO'
84
+ result = self._render('{{ "HeLlO"|upper }}')
85
+ assert result == expected
86
+
87
+ # TODO: Write test for urlencode
88
+ # TODO: Write test for urlize
89
+ # TODO: Write test for urlizetrunc
90
+ # TODO: Write test for wordcount
91
+ # TODO: Write test for wordwrap
92
+ # TODO: Write test for ljust
93
+ # TODO: Write test for rjust
94
+ # TODO: Write test for center
95
+ # TODO: Write test for cut
96
+ # TODO: Write test for escape
97
+ # TODO: Write test for force_escape
98
+ # TODO: Write test for linebreaks
99
+ # TODO: Write test for linebreaksbr
100
+ # TODO: Write test for safe
101
+ # TODO: Write test for safeseq
102
+ # TODO: Write test for striptags
103
+ # TODO: Write test for dictsort
104
+ # TODO: Write test for dictsortreversed
105
+
106
+ def test_trust_first(self):
107
+ expected = 'test1'
108
+ result = self._render(
109
+ '{{ value|first }}',
110
+ context={'value': ['test1', 'test2']},
111
+ )
112
+ assert result == expected
113
+
114
+ # TODO: Write test for first
115
+ # TODO: Write test for join
116
+ # TODO: Write test for last
117
+ # TODO: Write test for length
118
+ # TODO: Write test for length_is
119
+ # TODO: Write test for random
120
+ # TODO: Write test for slice
121
+ # TODO: Write test for unordered_list
122
+ # TODO: Write test for add
123
+ # TODO: Write test for get_digit
124
+ # TODO: Write test for date
125
+ # TODO: Write test for time
126
+ # TODO: Write test for timesince
127
+ # TODO: Write test for timeuntil
128
+
129
+ def test_trust_default(self):
130
+ expected = 'OK'
131
+ result = self._render(
132
+ '{{ value|default:"OK" }}',
133
+ context={'value': False},
134
+ )
135
+ assert result == expected
136
+
137
+ def test_trust_default_if_none(self):
138
+ expected = 'OK'
139
+ result = self._render(
140
+ '{{ value|default_if_none:"OK" }}',
141
+ context={'value': None},
142
+ )
143
+ assert result == expected
144
+
145
+ # TODO: Write test for divisibleby
146
+
147
+ def test_trust_yesno(self):
148
+ expected = 'No'
149
+ result = self._render(
150
+ '{{ value|yesno:"Yes,No" }}',
151
+ context={'value': False},
152
+ )
153
+ assert result == expected
154
+
155
+ # FIXME
156
+ # def test_trust_filesizeformat(self):
157
+ # expected = '117.7 MB'
158
+ # result = self._render('{{ "123456789"|filesizeformat }}')
159
+ # assert result == expected
160
+
161
+ def test_trust_pluralize(self):
162
+ expected = 'tests'
163
+ result = self._render(
164
+ 'test{{ value|pluralize }}',
165
+ context={'value': 2},
166
+ )
167
+ assert result == expected
168
+
169
+ def test_trust_phone2numeric(self):
170
+ expected = '800-2655328'
171
+ result = self._render('{{ "800-COLLECT"|phone2numeric }}')
172
+ assert result == expected
@@ -0,0 +1,27 @@
1
+ import pytest
2
+ from django.template.base import Template
3
+ from django.template.context import Context
4
+ from django.template.exceptions import TemplateSyntaxError
5
+
6
+ from django_safe_template_engine.engine import SafeTemplateEngine
7
+
8
+
9
+ class TestUntrustedFilters:
10
+ engine = SafeTemplateEngine()
11
+
12
+ def _render(self, template_code, context={}):
13
+ template = Template(template_code, engine=self.engine)
14
+ return template.render(Context(context))
15
+
16
+ def _msg_regex(self, filter_name):
17
+ return rf"^Invalid filter: '{filter_name}'"
18
+
19
+ def test_do_not_trust_pprint(self):
20
+ with pytest.raises(
21
+ TemplateSyntaxError,
22
+ match=self._msg_regex('pprint'),
23
+ ):
24
+ self._render(
25
+ '{{ test_list|pprint }}',
26
+ context={'test_list': []}
27
+ )
@@ -0,0 +1,45 @@
1
+ import pytest
2
+ from django.template.base import Template
3
+ from django.template.context import Context
4
+ from django.template.exceptions import TemplateSyntaxError
5
+
6
+ from django_safe_template_engine.engine import SafeTemplateEngine
7
+
8
+
9
+ class TestUntrustedLoaders:
10
+ engine = SafeTemplateEngine()
11
+
12
+ def _render(self, template_code, context={}):
13
+ template = Template(template_code, engine=self.engine)
14
+ return template.render(Context(context))
15
+
16
+ def _msg_regex(self, loader_name):
17
+ return rf"^Invalid block tag on line .+: '{loader_name}'"
18
+
19
+ def test_do_not_trust_block(self):
20
+ with pytest.raises(
21
+ TemplateSyntaxError,
22
+ match=self._msg_regex('block'),
23
+ ):
24
+ self._render('{% block content %}{% endblockcontent %}')
25
+
26
+ def test_do_not_trust_extends(self):
27
+ with pytest.raises(
28
+ TemplateSyntaxError,
29
+ match=self._msg_regex('extends'),
30
+ ):
31
+ self._render('{% extends "hacked.html" %}')
32
+
33
+ def test_do_not_trust_include(self):
34
+ with pytest.raises(
35
+ TemplateSyntaxError,
36
+ match=self._msg_regex('include'),
37
+ ):
38
+ self._render('{% include "hacked.html" %}')
39
+
40
+ def test_do_not_trust_static(self):
41
+ with pytest.raises(
42
+ TemplateSyntaxError,
43
+ match=self._msg_regex('static'),
44
+ ):
45
+ self._render('{% static "hacked.css" %}')
@@ -0,0 +1,11 @@
1
+ # Path to package:
2
+ ..
3
+
4
+ # Coding style / typing:
5
+ flake8
6
+ isort
7
+ mypy
8
+
9
+ # Test tools:
10
+ pytest
11
+ django == 3.0.*
@@ -0,0 +1 @@
1
+ SECRET_KEY = "123"
@@ -0,0 +1,211 @@
1
+ import pytest
2
+ from django.template.base import Template
3
+ from django.template.context import Context
4
+ from django.template.exceptions import TemplateSyntaxError
5
+
6
+ from django_safe_template_engine.engine import SafeTemplateEngine
7
+
8
+
9
+ class TestTrustedTags:
10
+ engine = SafeTemplateEngine()
11
+
12
+ def _render(self, template_code, context={}):
13
+ template = Template(template_code, engine=self.engine)
14
+ return template.render(Context(context))
15
+
16
+ def test_trust_autoescape(self):
17
+ expected = '<script></script>'
18
+ result = self._render(
19
+ '{% autoescape off %}'
20
+ '<script></script>'
21
+ '{% endautoescape %}',
22
+ )
23
+ assert result == expected
24
+
25
+ def test_trust_comment(self):
26
+ expected = 'Displayed'
27
+ result = self._render(
28
+ 'Displayed'
29
+ '{% comment %}'
30
+ 'Hidden'
31
+ '{% endcomment %}',
32
+ )
33
+ assert result == expected
34
+
35
+ def test_trust_cycle(self):
36
+ expected = (
37
+ '<span class="text1">First text</span>'
38
+ '<span class="text2">Second text</span>'
39
+
40
+ )
41
+ result = self._render(
42
+ '{% for item in items %}'
43
+ '<span class="{% cycle "text1" "text2" "text3" %}">{{ item }}</span>'
44
+ '{% endfor %}',
45
+ context={'items': ['First text', 'Second text']}
46
+ )
47
+ assert result == expected
48
+
49
+ def test_trust_filter(self):
50
+ expected = '1. ONE\n2. TWO\n3. THREE'
51
+ result = self._render(
52
+ '{% filter linenumbers|upper %}'
53
+ 'one\ntwo\nthree'
54
+ '{% endfilter %}',
55
+ context={'value': 'one\ntwo\nthree'},
56
+ )
57
+ assert result == expected
58
+
59
+ def test_filter_with_untrusted_filter(self):
60
+ with pytest.raises(
61
+ TemplateSyntaxError,
62
+ match=r"^Invalid filter: 'pprint'",
63
+ ):
64
+ self._render(
65
+ '{% filter pprint %}'
66
+ 'Unreachable due to usage of pprint filter'
67
+ '{% endfilter %}',
68
+ )
69
+
70
+ def test_trust_firstof(self):
71
+ expected = 'OK'
72
+ result = self._render(
73
+ '{% firstof empty "OK" %}',
74
+ context={'empty': ''},
75
+ )
76
+ assert result == expected
77
+
78
+ def test_trust_for(self):
79
+ expected = '123'
80
+ result = self._render(
81
+ '{% for item in items %}'
82
+ '{{ item }}'
83
+ '{% endfor %}',
84
+ context={'items': [1, 2, 3]},
85
+ )
86
+ assert result == expected
87
+
88
+ def test_trust_for_empty(self):
89
+ expected = 'No items'
90
+ result = self._render(
91
+ '{% for item in items %}'
92
+ '{{ item }}'
93
+ '{% empty %}'
94
+ 'No items'
95
+ '{% endfor %}',
96
+ context={'items': []},
97
+ )
98
+ assert result == expected
99
+
100
+ def test_trust_if(self):
101
+ expected = 'New messages'
102
+ result = self._render(
103
+ '{% if messages %}'
104
+ 'New messages'
105
+ '{% else %}'
106
+ 'No new messages'
107
+ '{% endif %}',
108
+ context={'messages': ['message1', 'message2']}
109
+ )
110
+ assert result == expected
111
+
112
+ def test_trust_if_with_trusted_filter(self):
113
+ expected = 'New messages'
114
+ result = self._render(
115
+ '{% if messages|length > 0 %}'
116
+ 'New messages'
117
+ '{% else %}'
118
+ 'No new messages'
119
+ '{% endif %}',
120
+ context={'messages': ['message1', 'message2']}
121
+ )
122
+ assert result == expected
123
+
124
+ def test_if_with_untrusted_filter(self):
125
+ with pytest.raises(
126
+ TemplateSyntaxError,
127
+ match=r"^Invalid filter: 'pprint'",
128
+ ):
129
+ self._render(
130
+ '{% if messages|pprint %}'
131
+ 'Unreachable due to usage of pprint filter'
132
+ '{% endif %}',
133
+ )
134
+
135
+ def test_trust_ifchanged(self):
136
+ expected = '123'
137
+ result = self._render(
138
+ '{% for number in numbers %}'
139
+ '{% ifchanged number %}{{ number }}{% endifchanged %}'
140
+ '{% endfor %}',
141
+ context={'numbers': [1, 1, 2, 3, 3]}
142
+ )
143
+ assert result == expected
144
+
145
+ def test_trust_lorem(self):
146
+ expected = 'Lorem ipsum'
147
+ result = self._render('{% lorem 1 b %}')
148
+ assert result.startswith(expected) is True
149
+
150
+ def test_trust_now(self):
151
+ expected = ['0', '1']
152
+ result = self._render('{% now "I" %}')
153
+ assert result in expected
154
+
155
+ # TODO: Write test for regroup
156
+
157
+ def test_trust_resetcycle(self):
158
+ expected = (
159
+ '<span class="text1">First text</span>'
160
+ '<span class="text1">Second text</span>'
161
+
162
+ )
163
+ result = self._render(
164
+ '{% for item in items %}'
165
+ '<span class="{% cycle "text1" "text2" "text3" %}">{{ item }}</span>'
166
+ '{% resetcycle %}'
167
+ '{% endfor %}',
168
+ context={'items': ['First text', 'Second text']}
169
+ )
170
+ assert result == expected
171
+
172
+ def test_trust_spaceless(self):
173
+ expected = '<p><strong>Test</strong></p>'
174
+ result = self._render(
175
+ '''{% spaceless %}
176
+ <p>
177
+ <strong>Test</strong>
178
+ </p>
179
+ {% endspaceless %}''',
180
+ )
181
+ assert result == expected
182
+
183
+ def test_trust_templatetag(self):
184
+ expected = '{%'
185
+ result = self._render('{% templatetag openblock %}')
186
+ assert result == expected
187
+
188
+ # TODO: Write test for url
189
+
190
+ def test_trust_verbatim(self):
191
+ expected = '{% test %}'
192
+ result = self._render(
193
+ '{% verbatim %}'
194
+ '{% test %}'
195
+ '{% endverbatim %}',
196
+ )
197
+ assert result == expected
198
+
199
+ def test_trust_widthratio(self):
200
+ expected = '88'
201
+ result = self._render('{% widthratio 175 200 100 %}')
202
+ assert result == expected
203
+
204
+ def test_trust_with(self):
205
+ expected = '1 / 2'
206
+ result = self._render(
207
+ '{% with alpha=1 beta=2 %}'
208
+ '{{ alpha }} / {{ beta }}'
209
+ '{% endwith %}',
210
+ )
211
+ assert result == expected
@@ -0,0 +1,59 @@
1
+ import pytest
2
+ from django.template.base import Template
3
+ from django.template.context import Context
4
+ from django.template.exceptions import TemplateSyntaxError
5
+
6
+ from django_safe_template_engine.engine import SafeTemplateEngine
7
+
8
+
9
+ class TestUntrustedTags:
10
+ engine = SafeTemplateEngine()
11
+
12
+ def _render(self, template_code, context={}):
13
+ template = Template(template_code, engine=self.engine)
14
+ return template.render(Context(context))
15
+
16
+ def _msg_regex(self, tag_name):
17
+ return rf"^Invalid block tag on line .+: '{tag_name}'"
18
+
19
+ def test_do_not_trust_block(self):
20
+ with pytest.raises(
21
+ TemplateSyntaxError,
22
+ match=self._msg_regex('block'),
23
+ ):
24
+ self._render('{% block main %}')
25
+
26
+ def test_do_not_trust_csrf_token(self):
27
+ with pytest.raises(
28
+ TemplateSyntaxError,
29
+ match=self._msg_regex('csrf_token'),
30
+ ):
31
+ self._render('{% csrf_token %}')
32
+
33
+ def test_do_not_trust_debug(self):
34
+ with pytest.raises(
35
+ TemplateSyntaxError,
36
+ match=self._msg_regex('debug'),
37
+ ):
38
+ self._render('{% debug %}')
39
+
40
+ def test_do_not_trust_extends(self):
41
+ with pytest.raises(
42
+ TemplateSyntaxError,
43
+ match=self._msg_regex('extends'),
44
+ ):
45
+ self._render('{% extends "hacked.html" %}')
46
+
47
+ def test_do_not_trust_include(self):
48
+ with pytest.raises(
49
+ TemplateSyntaxError,
50
+ match=self._msg_regex('include'),
51
+ ):
52
+ self._render('{% include "hacked.html" %}')
53
+
54
+ def test_do_not_trust_load(self):
55
+ with pytest.raises(
56
+ TemplateSyntaxError,
57
+ match=self._msg_regex('load'),
58
+ ):
59
+ self._render('{% load i18n %}')
@@ -0,0 +1,39 @@
1
+ import pytest
2
+ from django.core.exceptions import ValidationError
3
+
4
+ from django_safe_template_engine.validators import (
5
+ validate_safe_engine_template_syntax,
6
+ )
7
+
8
+
9
+ class TestValidateSafeEngineTemplateSyntax:
10
+
11
+ def _validate(self, template_code):
12
+ validate_safe_engine_template_syntax(template_code)
13
+
14
+ def test_valid_template(self):
15
+ self._validate(
16
+ '{% autoescape off %}'
17
+ '<script>{{ "i am a test"|title }}</script>'
18
+ '{% endautoescape %}'
19
+ )
20
+
21
+ def test_generic_invalid_syntax(self):
22
+ with pytest.raises(ValidationError):
23
+ self._validate('{% i_do_not_exist %}')
24
+ with pytest.raises(ValidationError):
25
+ self._validate('{{ test|i_do_not_exist }}')
26
+ with pytest.raises(ValidationError):
27
+ self._validate('{% if %} Unclosed if')
28
+
29
+ def test_invalid_template_untrusted_loader(self):
30
+ with pytest.raises(ValidationError):
31
+ self._validate('{% load i18n %}')
32
+
33
+ def test_invalid_template_untrusted_tag(self):
34
+ with pytest.raises(ValidationError):
35
+ self._validate('{% extends "hacked.html" %}')
36
+
37
+ def test_invalid_template_untrusted_filter(self):
38
+ with pytest.raises(ValidationError):
39
+ self._validate('{{ test_list|pprint }}')