今回のテーマは「テンプレートのフィルターを使う」です。前回テンプレートタグを作成しました。今回はテンプレート上で用いる独自フィルターを使用していきます。
※本ページはテンプレートタグを使ってサイドバーを作成するまで読まれた方を対象としています。そのためサンプルソースコードが省略されている場合があります。
Djangoテンプレートのフィルター
Djangoのテンプレートには他のフレームワーク同様にテンプレート上でフィルター処理を行えます。そのため、予め組み込まれた組み込みフィルターが用意されています。組み込みフィルターの種類については公式ドキュメントの組み込みタグとフィルタをご覧ください。例えば、truncatecharsという組込みフィルターがありますが、これは{{ context.hoge | truncatechars:10 }}と書くと10文字に切り詰めて表示するという機能を持つフィルターです。
カスタムフィルターを作る
ではカスタムフィルターを作成していきましょう。今回はコメントにURLが記載された場合リンクとして表示するフィルターを作成しましょう。thread/templatetags/threadfilters.pyを作成します。
thread/templatetags/threadfilters.py
from django.template import Library
register = Library()
@register.filter
def comment_filter(text):
return '
'.join(list(map(convert_url, text.split('\n'))))
def convert_url(text_line):
'''
URLリンク行をaタグ付きの行に変換
'''
if 'https://' in text_line or 'http://' in text_line:
return '<a href="' + text_line + '" target="_blank" rel="noopener noreferrer">' + text_line + '</a>'
else:
return text_line
register.filterメソッドは引数nameを省略すると関数の名前がフィルター名として適用されます。今回はcomment_filterですね。前回のテンプレートタグと同様にthreadfiltersをロードしてcomment_filterを適用することにします。
templates/thread/detail_topic.html
{% extends 'base/base.html' %}
{% block title %}トピック作成 - {{ block.super }}{% endblock %}
{% block content %}
{% load threadfilters %}
<div class="ui grid stackable">
<div class="eleven wide column">
<div class="ui breadcrumb">
<a href="{% url 'base:top' %}" class="section">TOP</a>
<i class="right angle icon divider"></i>
<a href="{% url 'thread:category' url_code=topic.category.url_code %}" class="section">{{topic.category.name}}</a>
<i class="right angle icon divider"></i>
<a class="active section">{{topic.title}}</a>
</div>
<div class="ui segment">
<div class="content">
<div class="header"><h3>{{topic.title}}</h3></div>
<p>{{topic.user_name}} - {{topic.created}}</p>
<div class="ui segment">
<p><pre>{{topic.message}}</pre></p>
</div>
</div>
</div>
<!--コメント表示-->
<div class="ui segment">
{% if comment_list %}
{% for comment in comment_list %}
<div class="ui segment secondary">
<p>{{comment.no}}. {{comment.user_name}}<br>{{comment.created}}</p>
{% if comment.pub_flg %}
<p>{{comment.message | comment_filter}}</p>
{% else %}
<p style="color: #aaa">このコメントは非表示となりました。</p>
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="ui warning message"><p>まだコメントはありません</p></div>
{% endif %}
</div>
<!--//コメント表示-->
<!--コメント投稿-->
<h4>コメント投稿</h4>
<div class="ui segment">
<form class="ui form" action="" method="POST">
{% csrf_token %}
{{form.as_p}}
<button class="ui button orange" type="submit">コメント投稿</button>
</form>
</div>
<!--//コメント投稿-->
</div>
{% include 'base/sidebar.html' %}
</div>
{% endblock %}
このようになります。{% load threadfilters %}でフィルターを読み込んでいます。{{comment.message | comment_filter}}の部分でコンテキストで渡された文字列を変換しています。ではブラウザで確認してみましょう。
URLを含んだ投稿をします。(今回のケースではURL行は改行される必要があります。)
変換された結果
あれ?ちょっとおかしな結果になりましたね。原因を考えていきましょう。
Djangoテンプレートエスケープ処理
確かに文字列は変換されていますが、HTMLタグがそのまま表示されてしまいました。これはどういうことでしょうか?実はDjangoテンプレートはデフォルトでエスケープ機能がONになっていて、コンテキストで渡された文字列はエスケープ処理が施されて出力されることになっています。PHPでいうとhtmlspecialchars関数が適用されたような状態です。これにより万が一コンテキストに悪意のあるスクリプトが渡されたとしても実行を防ぐ作用をしています。(ただし過信は禁物です。)
今回のようにフィルターでHTMLを生成する場合にはエスケープ機能をOFFにする必要があります。しかし、この機能を外す際にはプログラマーは十分に注意する必要があります。今回はbleachライブラリを使用してエスケープ処理をした後にHTML変換を書けることで対応してみます。
まずはbleachをインストールしましょう。
(venv)$ pip install bleach
次にthread/templatetags/threadfilters.pyを下記のように書き換えます。
thread/templatetags/threadfilters.py
from django.template import Library
import bleach
register = Library()
@register.filter
def comment_filter(text):
return '
'.join(list(map(convert_url, bleach.clean(text).split('\n'))))
def convert_url(text_line):
'''
URLリンク行をaタグ付きの行に変換
'''
if 'https://' in text_line or 'http://' in text_line:
return '$lt;a href="' + text_line + '" target="_blank" rel="noopener noreferrer">' + text_line + '</a>'
else:
return text_line
先で説明した通り、テンプレートの機能でエスケープ処理が行われないために、関数内部でエスケープ処理をしています。ではテンプレートを修正しましょう。
templates/thread/detail_topic.html(一部抜粋)
- <p>{{comment.message | comment_filter}}</p>
+ <p>{{comment.message | comment_filter | safe}}</p>
このようにsafeフィルターを付けることでテンプレートのエスケープ処理が外れます。これはプログラマが’safe’な文字列であることを保証するという意味です。その意味を考えたら気軽には使えませんよね。今回はbleach関数で手軽に事前エスケープで対応しましたが、実際はもっと気を配るケースが多いと思います。
ではブラウザでどのように表示されるかを確認してみましょう。URLリンクが想定通り出力されたでしょうか?
最後に
今回はフィルターのカスタマイズとDjangoテンプレートのエスケープ機能を見てきました。ネットで調べ物をしているとセキュリティに無頓着でユーザーの入力によってスクリプトが実行できてしまうような実装をしているサンプルも目にします。よく考えずに参考にすると痛い目を見ると思います。当然、この記事も鵜呑みにせずに使用する際には吟味しないとダメですよ。次回はjavascriptでAjax通信をすることを考えていきます。
Sponsored Link
convert_urlのreturnが
return ” + text_line + ”
となっているため、何も変わりませんでした。
return ‘‘ + text_line + ‘‘
とするのが正しいのでしょうか?
TTSSさん
コメントありがとうございます。
ご指摘の件ですが、タグがエスケープ処理されたことが原因でHTMLが正しく表示されていませんでした。
正しく修正しました。ご不便おかけして申し訳ありません。
また何か気付かれましたらご指摘お願いいたします。