diff options
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)) + |
