概要
- jinja2 を使って文字列をレンダリングする際に、テンプレートに対して渡したデータ自体が jinja2 のテンプレート形式であるときに、その内容もテンプレートとしてレンダリングを行いたい。
- 例
- テンプレート:
Hello, {{ sample_var }}
- データ:
{"sample_var": "{{ nested_var }}!", "nested_var": "world"}
- 期待する出力: "Hello, world!"
- テンプレート:
- 例
- 影響範囲を限定するため、jinja2 の filter で実現する。
- ※ユーザが入力するデータに対して使用する場合にはセキュリティ上の注意が必要です。xss (クロスサイトスクリプティング), SQLインジェクション等の脆弱性になることが考えられるため。
- 以下の例では、管理者が作成するマスタデータに対して行う処理とします。
実装
filter の実装
from jinja2 import Template, Environment, FileSystemLoader, pass_context
@pass_context
def recursive_render(context, value, max_iterations=10, **kwargs):
template = Template(value)
result = template.render(**context, **kwargs)
for _ in range(max_iterations):
new_result = Template(result).render(**context, **kwargs)
if new_result == result:
break
result = new_result
return result
使用方法
サンプル
env = Environment(loader=FileSystemLoader("."))
env.filters['recursive_render'] = recursive_render
template_str = """
{{ var1 | recursive_render }} is {{ var2 }}
"""
template = env.from_string(template_str)
context = {
"var1": "{{ nested_var }}",
"var2": "awesome",
"nested_var": "Python"
}
output = template.render(**context)
print(output)
出力
Python is awesome
テンプレート内で定義される変数は filter に引数として渡す
for ループなどによって、テンプレート内で定義される変数については、filter 使用時に引数として渡す必要がある。
@pass_context
デコレータによって context
変数に渡される内容は、render()
の引数のみであるため。
失敗例
template_str = """\
{% for member in members %}
{{ member.name }}
{{ member.introduction | recursive_render }}
{% endfor %}
"""
template = env.from_string(template_str)
context = {
"members": [
{"name": "テスト1", "introduction": "はじめまして、{{ member.name }} です。"},
{"name": "テスト2", "introduction": "はじめまして、{{ member.name }} です。"},
]
}
output = template.render(**context)
print(output)
-> jinja2.exceptions.UndefinedError: 'member' is undefined
例外が発生
成功例 (recursive_render フィルタに引数を渡すように変更)
template_str = """\
{% for member in members %}
{{ member.name }}
{{ member.introduction | recursive_render(member=member) }}
{% endfor %}
"""
template = env.from_string(template_str)
context = {
"members": [
{"name": "テスト1", "introduction": "はじめまして、{{ member.name }} です。"},
{"name": "テスト2", "introduction": "はじめまして、{{ member.name }} です。"},
]
}
output = template.render(**context)
print(output)
出力
テスト1
はじめまして、テスト1 です。
テスト2
はじめまして、テスト2 です。
最後に
概要に記載しましたが改めて、ユーザが入力するデータに対して使用する際にはセキュリティ上の注意が必要です。
少なくとも上記の実装では context で渡されるデータに、recursive_render する変数の中身がアクセスできてしまいます。その際には、@pass_context
デコレータを外しアクセスできる変数を制限するなどの対応が必要になると考えられます。