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.
- django_safe_template_engine-1.0.0/.flake8 +7 -0
- django_safe_template_engine-1.0.0/.github/workflows/build.yml +30 -0
- django_safe_template_engine-1.0.0/.gitignore +135 -0
- django_safe_template_engine-1.0.0/LICENSE +28 -0
- django_safe_template_engine-1.0.0/PKG-INFO +166 -0
- django_safe_template_engine-1.0.0/README.md +118 -0
- django_safe_template_engine-1.0.0/isort.cfg +2 -0
- django_safe_template_engine-1.0.0/mypy.ini +8 -0
- django_safe_template_engine-1.0.0/pyproject.toml +27 -0
- django_safe_template_engine-1.0.0/run_tests.py +7 -0
- django_safe_template_engine-1.0.0/src/django_safe_template_engine/__init__.py +0 -0
- django_safe_template_engine-1.0.0/src/django_safe_template_engine/engine.py +16 -0
- django_safe_template_engine-1.0.0/src/django_safe_template_engine/trusted_filters.py +72 -0
- django_safe_template_engine-1.0.0/src/django_safe_template_engine/trusted_tags.py +27 -0
- django_safe_template_engine-1.0.0/src/django_safe_template_engine/validators.py +11 -0
- django_safe_template_engine-1.0.0/tests/__init__.py +0 -0
- django_safe_template_engine-1.0.0/tests/filters/__init__.py +0 -0
- django_safe_template_engine-1.0.0/tests/filters/test_trusted_filters.py +172 -0
- django_safe_template_engine-1.0.0/tests/filters/test_untrusted_filters.py +27 -0
- django_safe_template_engine-1.0.0/tests/loaders/__init__.py +0 -0
- django_safe_template_engine-1.0.0/tests/loaders/test_untrusted_loaders.py +45 -0
- django_safe_template_engine-1.0.0/tests/requirements.txt +11 -0
- django_safe_template_engine-1.0.0/tests/settings.py +1 -0
- django_safe_template_engine-1.0.0/tests/tags/__init__.py +0 -0
- django_safe_template_engine-1.0.0/tests/tags/test_trusted_tags.py +211 -0
- django_safe_template_engine-1.0.0/tests/tags/test_untrusted_tags.py +59 -0
- django_safe_template_engine-1.0.0/tests/test_validators.py +39 -0
|
@@ -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
|
+

|
|
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
|
+

|
|
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,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"
|
|
File without changes
|
|
@@ -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
|
|
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 = "['1', '2', '3']"
|
|
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
|
+
)
|
|
File without changes
|
|
@@ -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 @@
|
|
|
1
|
+
SECRET_KEY = "123"
|
|
File without changes
|
|
@@ -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 }}')
|