urlencode と、escape を間違えて、ひどい思いをしたので記載します。
基本的に、この記事上登場するのは、以下スクリプトに記載された実装の話になります。
django/html.py at master · django/django
django/defaultfilters.py at master · django/django


前提

以下の環境で作業は実施しています。

  • OS

    % sw_vers 
    ProductName:    Mac OS X
    ProductVersion: 10.12.6
    BuildVersion:   16G29
    

  • python の verion

    % python -V
    Python 2.7.10   
    

  • django の version

    % pip list | grep Django
    Django (1.10.7)
    


参考


django の デフォルトの動作について

基本的にデフォルト動作だと、エスケープが発動します。
発動を止めるには、{% autoescape off %}使うか、safe フィルタ を使う必要があります。
動作については、以下の記事にまとまっております。
メモ】Djangoでhtmlをエスケープせずそのまま出力する - 気ままなタンス プログラミングなどのノートブック


escape について

HTML で 特殊文字として扱われる 文字列エスケープするメソッドです。
基本的に、django のデフォルト動作でエスケープされるので、使用する機会があるとしたら、

{% autoescape off %}
...........
{% endautoescape %}
囲まれた処理で {{ blog_post.title|escape}}して記載するのかと思います。

&入力すると、

from __future__ import print_function
from django.utils import html
print(html.escape("&"))

エスケープされ、

&
なります。

>

>入力すると、

from __future__ import print_function
from django.utils import html
print(html.escape(">"))

エスケープされ

>
なります。

HTML の構文として、問題が現実的に出るであろう < > ' " & のみを特殊文字列に置換します。

from __future__ import print_function
from django.utils import html
print(html.escape("<>'\"&"))
出力は以下になります。

&lt;&gt;&#39;&quot;&amp;
この動作なにかでみたことあると思ったのですが、Struts <bean:write > タグの挙動でした。

Struts リファレンス<bean:write >
HTML タグのエスケープ方法は10年くらいは変わっていなさそうです。
それだけ息の長い仕様なのだと思いました。

ちなみに、wicket の escape メソッドもデフォルト挙動では、django、struts と同じ挙動となります。
追加の機能で、オプションでスペースのエスケープ有無、ユニコード文字列のエスケープ有無を選択できるようです。
wicket/Strings.java at master · apache/wicket escapeMarkup メソッドがエスケープ処理を行います。

django の escape メソッド自体の実装を見ると、以下のような実装になっています。
HTML で 特殊文字として扱われる文字列をエスケープして、エスケープした後に、mark_safe実行し、2回 escape されないようにしています。
ただ、escape 2度呼び出すと、それは強制 escape になり、2度目だったら escape しない場合は、conditional_escape使う必要があります。

  • escape メソッド
    _html_escapes = {
        ord('&'): '&amp;',
        ord('<'): '&lt;',
        ord('>'): '&gt;',
        ord('"'): '&quot;',
        ord("'"): '&#39;',
    }
    
    @keep_lazy(str, SafeText)
    def escape(text):
        """
        Return the given text with ampersands, quotes and angle brackets encoded
        for use in HTML.
        Always escape input, even if it's already escaped and marked as such.
        This may result in double-escaping. If this is a concern, use
        conditional_escape() instead.
        """
        return mark_safe(str(text).translate(_html_escapes))
    

conditional_escape について

ドキュメント読んでいたら見つけました。
escape とは似ているが少し違うと記載されています。
違いは、一度エスケープしていたら、しない。<wbr>
扱う値がエスケープされているかされていないかが把握できない時に使用するメソッドかと思います。


escapejsについて

template 内に javascript を書き付けたい場合があります。
その際に使用するのがこのフィルターです。
ダブルクォート、シングルクォートあたりを置換するだけかと思っていましたが、実装上はその他の文字列もエスケープの対象になっていました。

  • escapejsの対象文字列の抜粋
    _js_escapes = {
        ord('\\'): '\\u005C',
        ord('\''): '\\u0027',
        ord('"'): '\\u0022',
        ord('>'): '\\u003E',
        ord('<'): '\\u003C',
        ord('&'): '\\u0026',
        ord('='): '\\u003D',
        ord('-'): '\\u002D',
        ord(';'): '\\u003B',
        ord('`'): '\\u0060',
        ord('\u2028'): '\\u2028',
        ord('\u2029'): '\\u2029'
    }
    
    # Escape every ASCII character with a value less than 32.
    _js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32))
    

実際の変換には、上記の辞書を、translate メソッドの引数として使っています。
* escapejs

@keep_lazy(str, SafeText)
def escapejs(value):
    """Hex encode characters for use in JavaScript strings."""
    return mark_safe(str(value).translate(_js_escapes))
私は、translate知りませんでしたが、以下が参考になりました。
Pythonでの文字列置換をマスターする - orangain flavor

また、escape置換文字列をHTMLの文字実体参照に変換していますが、escapejs は、置換対象文字列をUnicodeコードポイントに変換しています。
発動想定箇所の違いで、実装の具合が異なりますね。
参考記事へのリンクを貼りますが、私は記載内容についてまだよく理解できておりません。
JavaScriptでのサロゲートペア文字列のメモ - Qiita


escape、escapejs 後に、autoescape は発動しない。

Built-in template tags and filters | Django documentation | Django
記載がありますが、escape 後、escapejs 後は、autoescapeによるescapeは行われず、conditional_escape の同様の動作となるようです。


urlencodeについて

内部実装は以下のようになっています。
urllib.parse.quote使ってますね。

  • urlencodeメソッド
    @register.filter(is_safe=False)
    @stringfilter
    def urlencode(value, safe=None):
        """
        Escape a value for use in a URL.
        The ``safe`` parameter determines the characters which should not be
        escaped by Python's quote() function. If not provided, use the default safe
        characters (but an empty string can be provided when *all* characters
        should be escaped).
        """
        kwargs = {}
        if safe is not None:
            kwargs['safe'] = safe
        return quote(value, **kwargs)
    

urlencode の対象文字列は、URIで使用できる文字 - CyberLibrarian記載されている文字種と、後は全角文字列だと思います。
urlencode 処理は mark_safe していないので、autoescape の対象にはなりますが、< 等がパーセントエンコーディングの対象となるため結局はautoescape は発動しない動作となりそうです。

セキュリティ観点で念のために、autoescape にも処理を流すようにしているのかと思います。


まとめ

一言、エスケープとか、エンコードはややこしい。
SQL 等のクエリ系のもあるでしょうし、どこでやるべきか等は揉めそうですが、mark_safeいう考えは参考になりました。
以上です。

コメント