diff options
| author | Malcolm Tredinnick <malcolm.tredinnick@gmail.com> | 2007-11-14 12:58:53 +0000 |
|---|---|---|
| committer | Malcolm Tredinnick <malcolm.tredinnick@gmail.com> | 2007-11-14 12:58:53 +0000 |
| commit | 356662cf74c99fac90afb0f5e6aac8d2d573e62a (patch) | |
| tree | 6ee45dfcb9c91e1184dcc73751e0b856892451ed /django/utils | |
| parent | babfe78494028415b0e5f74ec2ca9b66506e8d34 (diff) | |
Implemented auto-escaping of variable output in templates. Fully controllable by template authors and it's possible to write filters and templates that simulataneously work in both auto-escaped and non-auto-escaped environments if you need to. Fixed #2359
See documentation in templates.txt and templates_python.txt for how everything
works.
Backwards incompatible if you're inserting raw HTML output via template variables.
Based on an original design from Simon Willison and with debugging help from Michael Radziej.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@6671 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django/utils')
| -rw-r--r-- | django/utils/encoding.py | 6 | ||||
| -rw-r--r-- | django/utils/html.py | 33 | ||||
| -rw-r--r-- | django/utils/safestring.py | 124 |
3 files changed, 155 insertions, 8 deletions
diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 4bda9caa50..2ab0db7432 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -3,6 +3,7 @@ import urllib import datetime from django.utils.functional import Promise +from django.utils.safestring import SafeData, mark_safe class DjangoUnicodeDecodeError(UnicodeDecodeError): def __init__(self, obj, *args): @@ -51,7 +52,10 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): else: s = unicode(str(s), encoding, errors) elif not isinstance(s, unicode): - s = unicode(s, encoding, errors) + # Note: We use .decode() here, instead of unicode(s, encoding, + # errors), so that if s is a SafeString, it ends up being a + # SafeUnicode at the end. + s = s.decode(encoding, errors) except UnicodeDecodeError, e: raise DjangoUnicodeDecodeError(s, *e.args) return s diff --git a/django/utils/html.py b/django/utils/html.py index ebd04d1b3c..8eeaa66330 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -3,6 +3,7 @@ import re import string +from django.utils.safestring import SafeData, mark_safe from django.utils.encoding import force_unicode from django.utils.functional import allow_lazy @@ -27,16 +28,28 @@ del x # Temporary variable def escape(html): "Return the given HTML with ampersands, quotes and carets encoded." - return force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') + return mark_safe(force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) escape = allow_lazy(escape, unicode) -def linebreaks(value): - "Convert newlines into <p> and <br />s." +def conditional_escape(html): + """ + Similar to escape(), except that it doesn't operate on pre-escaped strings. + """ + if isinstance(html, SafeData): + return html + else: + return escape(html) + +def linebreaks(value, autoescape=False): + "Converts newlines into <p> and <br />s" value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines paras = re.split('\n{2,}', value) - paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras] + if autoescape: + paras = [u'<p>%s</p>' % escape(p.strip()).replace('\n', '<br />') for p in paras] + else: + paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras] return u'\n\n'.join(paras) -linebreaks = allow_lazy(linebreaks, unicode) +linebreaks = allow_lazy(linebreaks, unicode) def strip_tags(value): "Return the given HTML with all tags stripped." @@ -58,7 +71,7 @@ def fix_ampersands(value): return unencoded_ampersands_re.sub('&', force_unicode(value)) fix_ampersands = allow_lazy(fix_ampersands, unicode) -def urlize(text, trim_url_limit=None, nofollow=False): +def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): """ Convert any URLs in text into clickable links. @@ -72,13 +85,19 @@ def urlize(text, trim_url_limit=None, nofollow=False): If nofollow is True, the URLs in link text will get a rel="nofollow" attribute. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + if autoescape: + trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x) + else: + trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + safe_input = isinstance(text, SafeData) words = word_split_re.split(force_unicode(text)) nofollow_attr = nofollow and ' rel="nofollow"' or '' for i, word in enumerate(words): match = punctuation_re.match(word) if match: lead, middle, trail = match.groups() + if safe_input: + middle = mark_safe(middle) if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \ len(middle) > 0 and middle[0] in string.letters + string.digits and \ (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))): diff --git a/django/utils/safestring.py b/django/utils/safestring.py new file mode 100644 index 0000000000..c7234af0f3 --- /dev/null +++ b/django/utils/safestring.py @@ -0,0 +1,124 @@ +""" +Functions for working with "safe strings": strings that can be displayed safely +without further escaping in HTML. Marking something as a "safe string" means +that the producer of the string has already turned characters that should not +be interpreted by the HTML engine (e.g. '<') into the appropriate entities. +""" +from django.utils.functional import curry, Promise + +class EscapeData(object): + pass + +class EscapeString(str, EscapeData): + """ + A string that should be HTML-escaped when output. + """ + pass + +class EscapeUnicode(unicode, EscapeData): + """ + A unicode object that should be HTML-escaped when output. + """ + pass + +class SafeData(object): + pass + +class SafeString(str, SafeData): + """ + A string subclass that has been specifically marked as "safe" (requires no + further escaping) for HTML output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe string with another safe string or safe unicode + object is safe. Otherwise, the result is no longer safe. + """ + if isinstance(rhs, SafeUnicode): + return SafeUnicode(self + rhs) + elif isinstance(rhs, SafeString): + return SafeString(self + rhs) + else: + return super(SafeString, self).__add__(rhs) + + def __str__(self): + return self + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = str.encode) + decode = curry(_proxy_method, method = str.decode) + +class SafeUnicode(unicode, SafeData): + """ + A unicode subclass that has been specifically marked as "safe" for HTML + output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe unicode object with another safe string or safe + unicode object is safe. Otherwise, the result is no longer safe. + """ + if isinstance(rhs, SafeData): + return SafeUnicode(self + rhs) + else: + return super(SafeUnicode, self).__add__(rhs) + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = unicode.encode) + decode = curry(_proxy_method, method = unicode.decode) + +def mark_safe(s): + """ + Explicitly mark a string as safe for (HTML) output purposes. The returned + object can be used everywhere a string or unicode object is appropriate. + + Can be called multiple times on a single string. + """ + if isinstance(s, SafeData): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return SafeString(s) + if isinstance(s, (unicode, Promise)): + return SafeUnicode(s) + return SafeString(str(s)) + +def mark_for_escaping(s): + """ + Explicitly mark a string as requiring HTML escaping upon output. Has no + effect on SafeData subclasses. + + Can be called multiple times on a single string (the resulting escaping is + only applied once). + """ + if isinstance(s, (SafeData, EscapeData)): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return EscapeString(s) + if isinstance(s, (unicode, Promise)): + return EscapeUnicode(s) + return EscapeString(str(s)) + |
