Django

Code

Ticket #2359: autoescape.diff

File autoescape.diff, 44.4 kB (added by mtredinnick, 2 years ago)

Core changes and test suite

  • django_autoescape/django/contrib/markup/templatetags/markup.py

    old new  
    1616 
    1717from django import template 
    1818from django.conf import settings 
     19from django.utils.safestring import mark_safe 
    1920 
    2021register = template.Library() 
    2122 
     
    2526    except ImportError: 
    2627        if settings.DEBUG: 
    2728            raise template.TemplateSyntaxError, "Error in {% textile %} filter: The Python textile library isn't installed." 
    28         return value 
     29        return mark_safe(value) 
    2930    else: 
    30         return textile.textile(value, encoding=settings.DEFAULT_CHARSET, output=settings.DEFAULT_CHARSET) 
     31        return mark_safe(textile.textile(value, encoding=settings.DEFAULT_CHARSET, output=settings.DEFAULT_CHARSET)) 
     32textile.is_safe = True 
    3133 
    3234def markdown(value): 
    3335    try: 
     
    3537    except ImportError: 
    3638        if settings.DEBUG: 
    3739            raise template.TemplateSyntaxError, "Error in {% markdown %} filter: The Python markdown library isn't installed." 
    38         return value 
     40        return mark_safe(value) 
    3941    else: 
    40         return markdown.markdown(value) 
     42        return mark_safe(markdown.markdown(value)) 
     43markdown.is_safe = True 
    4144 
    4245def restructuredtext(value): 
    4346    try: 
     
    4548    except ImportError: 
    4649        if settings.DEBUG: 
    4750            raise template.TemplateSyntaxError, "Error in {% restructuredtext %} filter: The Python docutils library isn't installed." 
    48         return value 
     51        return mark_safe(value) 
    4952    else: 
    5053        docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) 
    5154        parts = publish_parts(source=value, writer_name="html4css1", settings_overrides=docutils_settings) 
    52         return parts["fragment"] 
     55        return mark_safe(parts["fragment"]) 
     56restructuredtext.is_safe = True 
    5357 
    5458register.filter(textile) 
    5559register.filter(markdown) 
  • django_autoescape/django/template/context.py

    old new  
    99 
    1010class Context(object): 
    1111    "A stack container for variable context" 
     12 
     13    autoescape = False 
     14 
    1215    def __init__(self, dict_=None): 
    1316        dict_ = dict_ or {} 
    1417        self.dicts = [dict_] 
     
    9598            processors = tuple(processors) 
    9699        for processor in get_standard_processors() + processors: 
    97100            self.update(processor(request)) 
     101 
     102class SafeContext(Context): 
     103    "A Context derivative that autoescapes by default." 
     104    autoescape = True 
     105 
     106class SafeRequestContext(RequestContext): 
     107    autoescape = True 
  • django_autoescape/django/template/defaultfilters.py

    old new  
    33from django.template import resolve_variable, Library 
    44from django.conf import settings 
    55from django.utils.translation import gettext 
     6from django.utils.safestring import mark_safe, SafeData 
    67import re 
    78import random as random_module 
    89 
     
    1617def addslashes(value): 
    1718    "Adds slashes - useful for passing strings to JavaScript, for example." 
    1819    return value.replace('"', '\\"').replace("'", "\\'") 
     20addslashes.is_safe = True 
    1921 
    2022def capfirst(value): 
    2123    "Capitalizes the first character of the value" 
    2224    value = str(value) 
    2325    return value and value[0].upper() + value[1:] 
     26capfirst.is_safe = True 
    2427 
    2528def fix_ampersands(value): 
    2629    "Replaces ampersands with ``&`` entities" 
    2730    from django.utils.html import fix_ampersands 
    2831    return fix_ampersands(value) 
     32fix_ampersands.is_safe = True 
    2933 
    3034def floatformat(text): 
    3135    """ 
     
    4044    if m: 
    4145        return '%.1f' % f 
    4246    else: 
    43         return '%d' % int(f) 
     47        return mark_safe('%d' % int(f)) 
     48floatformat.is_safe = True 
    4449 
    45 def linenumbers(value): 
     50def linenumbers(value, autoescape = None): 
    4651    "Displays text with line numbers" 
    4752    from django.utils.html import escape 
    4853    lines = value.split('\n') 
    4954    # Find the maximum width of the line count, for use with zero padding string format command 
    5055    width = str(len(str(len(lines)))) 
    51     for i, line in enumerate(lines): 
    52         lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line)) 
    53     return '\n'.join(lines) 
     56    if not autoescape or isinstance(value, SafeData): 
     57        for i, line in enumerate(lines): 
     58            lines[i] = ("%0" + width  + "d. %s") % (i + 1, line) 
     59    else: 
     60        for i, line in enumerate(lines): 
     61            lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line)) 
     62    return mark_safe('\n'.join(lines)) 
     63linenumbers.is_safe = True 
     64linenumbers.needs_autoescape = True 
    5465 
    5566def lower(value): 
    5667    "Converts a string into all lowercase" 
    5768    return value.lower() 
     69lower.is_safe = True 
    5870 
    5971def make_list(value): 
    6072    """ 
     
    6274    digits. For a string, it's a list of characters. 
    6375    """ 
    6476    return list(str(value)) 
     77make_list.is_safe = False 
    6578 
    6679def slugify(value): 
    6780    "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" 
    6881    value = re.sub('[^\w\s-]', '', value).strip().lower() 
    69     return re.sub('[-\s]+', '-', value) 
     82    return mark_safe(re.sub('[-\s]+', '-', value)) 
     83slugify.is_safe = True 
    7084 
    7185def stringformat(value, arg): 
    7286    """ 
     
    8195        return ("%" + arg) % value 
    8296    except (ValueError, TypeError): 
    8397        return "" 
     98stringformat.is_safe = True 
    8499 
    85100def title(value): 
    86101    "Converts a string into titlecase" 
    87102    return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) 
     103title.is_safe = False 
    88104 
    89105def truncatewords(value, arg): 
    90106    """ 
     
    100116    if not isinstance(value, basestring): 
    101117        value = str(value) 
    102118    return truncate_words(value, length) 
     119truncatewords.is_safe = True 
    103120 
    104121def upper(value): 
    105122    "Converts a string into all uppercase" 
    106123    return value.upper() 
     124upper.is_safe = False 
    107125 
    108126def urlencode(value): 
    109127    "Escapes a value for use in a URL" 
    110128    import urllib 
    111129    return urllib.quote(value) 
     130urlencode.is_safe = False 
    112131 
    113132def urlize(value): 
    114133    "Converts URLs in plain text into clickable links" 
    115134    from django.utils.html import urlize 
    116     return urlize(value, nofollow=True) 
     135    return mark_safe(urlize(value, nofollow=True)) 
     136urlize.is_safe = True 
    117137 
    118138def urlizetrunc(value, limit): 
    119139    """ 
     
    123143    Argument: Length to truncate URLs to. 
    124144    """ 
    125145    from django.utils.html import urlize 
    126     return urlize(value, trim_url_limit=int(limit), nofollow=True) 
     146    return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True)) 
     147urlize.is_safe = True 
    127148 
    128149def wordcount(value): 
    129150    "Returns the number of words" 
    130151    return len(value.split()) 
     152wordcount.is_safe = False 
    131153 
    132154def wordwrap(value, arg): 
    133155    """ 
     
    137159    """ 
    138160    from django.utils.text import wrap 
    139161    return wrap(str(value), int(arg)) 
     162wordwrap.is_safe = True 
    140163 
    141164def ljust(value, arg): 
    142165    """ 
     
    145168    Argument: field size 
    146169    """ 
    147170    return str(value).ljust(int(arg)) 
     171ljust.is_safe = True 
    148172 
    149173def rjust(value, arg): 
    150174    """ 
     
    153177    Argument: field size 
    154178    """ 
    155179    return str(value).rjust(int(arg)) 
     180rjust.is_safe = True 
    156181 
    157182def center(value, arg): 
    158183    "Centers the value in a field of a given width" 
    159184    return str(value).center(int(arg)) 
     185center.is_safe = True 
    160186 
    161187def cut(value, arg): 
    162188    "Removes all values of arg from the given string" 
    163189    return value.replace(arg, '') 
     190cut.is_safe = False 
    164191 
    165192################### 
    166193# HTML STRINGS    # 
     
    168195 
    169196def escape(value): 
    170197    "Escapes a string's HTML" 
    171     from django.utils.html import escape 
    172     return escape(value) 
     198    if not isinstance(value, SafeData): 
     199        from django.utils.html import escape 
     200        return mark_safe(escape(value)) 
     201    else: 
     202        return value 
     203escape.is_safe = True 
    173204 
    174 def linebreaks(value): 
     205def linebreaks(value, autoescape = None): 
    175206    "Converts newlines into <p> and <br />s" 
    176207    from django.utils.html import linebreaks 
    177     return linebreaks(value) 
     208    autoescape = autoescape and not isinstance(value, SafeData) 
     209    return mark_safe(linebreaks(value, autoescape)) 
     210linebreaks.is_safe = True 
     211linebreaks.needs_autoescape = True 
    178212 
    179 def linebreaksbr(value): 
     213def linebreaksbr(value, autoescape = None): 
    180214    "Converts newlines into <br />s" 
    181     return value.replace('\n', '<br />') 
     215    if autoescape and not isinstance(value, SafeData): 
     216        from django.utils.html import escape 
     217        data = escape(value) 
     218    else: 
     219        data = value 
     220    return mark_safe(data.replace('\n', '<br />')) 
     221linebreaksbr.is_safe = True 
     222linebreaksbr.needs_autoescape = True 
     223 
     224def noescape(value): 
     225    "Marks the value as a string that should not be auto-escaped." 
     226    from django.utils.safestring import mark_safe 
     227    return mark_safe(value) 
     228noescape.is_safe = True 
    182229 
    183230def removetags(value, tags): 
    184231    "Removes a space separated list of [X]HTML tags from the output" 
     
    189236    value = starttag_re.sub('', value) 
    190237    value = endtag_re.sub('', value) 
    191238    return value 
     239removetags.is_safe = True 
    192240 
    193241def striptags(value): 
    194242    "Strips all [X]HTML tags" 
     
    196244    if not isinstance(value, basestring): 
    197245        value = str(value) 
    198246    return strip_tags(value) 
     247striptags.is_safe = True 
    199248 
    200249################### 
    201250# LISTS           # 
     
    209258    decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] 
    210259    decorated.sort() 
    211260    return [item[1] for item in decorated] 
     261dictsort.is_safe = False 
    212262 
    213263def dictsortreversed(value, arg): 
    214264    """ 
     
    219269    decorated.sort() 
    220270    decorated.reverse() 
    221271    return [item[1] for item in decorated] 
     272dictsortreversed.is_safe = False 
    222273 
    223274def first(value): 
    224275    "Returns the first item in a list" 
     
    226277        return value[0] 
    227278    except IndexError: 
    228279        return '' 
     280first.is_safe = True 
    229281 
    230282def join(value, arg): 
    231283    "Joins a list with a string, like Python's ``str.join(list)``" 
    232284    try: 
    233         return arg.join(map(str, value)) 
     285        data = arg.join(map(str, value)) 
    234286    except AttributeError: # fail silently but nicely 
    235287        return value 
     288    safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData), value, True) 
     289    if safe_args: 
     290        return mark_safe(data) 
     291    else: 
     292        return data 
     293join.is_safe = True 
    236294 
    237295def length(value): 
    238296    "Returns the length of the value - useful for lists" 
    239297    return len(value) 
     298length.is_safe = False 
    240299 
    241300def length_is(value, arg): 
    242301    "Returns a boolean of whether the value's length is the argument" 
    243302    return len(value) == int(arg) 
     303length.is_safe = False 
    244304 
    245305def random(value): 
    246306    "Returns a random item from the list" 
    247307    return random_module.choice(value) 
     308length.is_safe = True 
    248309 
    249310def slice_(value, arg): 
    250311    """ 
     
    265326 
    266327    except (ValueError, TypeError): 
    267328        return value # Fail silently. 
     329slice_.is_safe = True 
    268330 
    269 def unordered_list(value): 
     331def unordered_list(value, autoescape = None): 
    270332    """ 
    271333    Recursively takes a self-nested list and returns an HTML unordered list -- 
    272334    WITHOUT opening and closing <ul> tags. 
     
    287349        </ul> 
    288350        </li> 
    289351    """ 
     352    if autoescape: 
     353        from django.utils.html import conditional_escape 
     354        escaper = conditional_escape 
     355    else: 
     356        escaper = lambda x: x 
     357 
    290358    def _helper(value, tabs): 
    291359        indent = '\t' * tabs 
    292360        if value[1]: 
    293             return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, value[0], indent, 
     361            return '%s<li>%s\n%s<ul>\n%s\n%s</ul>\n%s</li>' % (indent, escaper(value[0]), indent, 
    294362                '\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent) 
    295363        else: 
    296             return '%s<li>%s</li>' % (indent, value[0]) 
    297     return _helper(value, 1) 
     364            return '%s<li>%s</li>' % (indent, escaper(value[0])) 
     365    return mark_safe(_helper(value, 1)) 
     366unordered_list.is_safe = True 
     367unordered_list.needs_autoescape = True 
    298368 
    299369################### 
    300370# INTEGERS        # 
     
    303373def add(value, arg): 
    304374    "Adds the arg to the value" 
    305375    return int(value) + int(arg) 
     376add.is_safe = False 
    306377 
    307378def get_digit(value, arg): 
    308379    """ 
     
    322393        return int(str(value)[-arg]) 
    323394    except IndexError: 
    324395        return 0 
     396get_digit.is_safe = False 
    325397 
    326398################### 
    327399# DATES           # 
     
    335407    if arg is None: 
    336408        arg = settings.DATE_FORMAT 
    337409    return format(value, arg) 
     410date.is_safe = False 
    338411 
    339412def time(value, arg=None): 
    340413    "Formats a time according to the given format" 
     
    344417    if arg is None: 
    345418        arg = settings.TIME_FORMAT 
    346419    return time_format(value, arg) 
     420time.is_safe = False 
    347421 
    348422def timesince(value, arg=None): 
    349423    'Formats a date as the time since that date (i.e. "4 days, 6 hours")' 
     
    353427    if arg: 
    354428        return timesince(arg, value) 
    355429    return timesince(value) 
     430timesince.is_safe = False 
    356431 
    357432def timeuntil(value, arg=None): 
    358433    'Formats a date as the time until that date (i.e. "4 days, 6 hours")' 
     
    363438    if arg: 
    364439        return timesince(arg, value) 
    365440    return timesince(datetime.now(), value) 
     441timeuntil.is_safe = False 
    366442 
    367443################### 
    368444# LOGIC           # 
     
    371447def default(value, arg): 
    372448    "If value is unavailable, use given default" 
    373449    return value or arg 
     450default.is_safe = False 
    374451 
    375452def default_if_none(value, arg): 
    376453    "If value is None, use given default" 
    377454    if value is None: 
    378455        return arg 
    379456    return value 
     457default_if_none.is_safe = False 
    380458 
    381459def divisibleby(value, arg): 
    382460    "Returns true if the value is devisible by the argument" 
    383461    return int(value) % int(arg) == 0 
     462divisibleby.is_safe = False 
    384463 
    385464def yesno(value, arg=None): 
    386465    """ 
     
    411490    if value: 
    412491        return yes 
    413492    return no 
     493yesno.is_safe = False 
    414494 
    415495################### 
    416496# MISC            # 
     
    429509    if bytes < 1024 * 1024 * 1024: 
    430510        return "%.1f MB" % (bytes / (1024 * 1024)) 
    431511    return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) 
     512filesizeformat.is_safe = True 
    432513 
    433514def pluralize(value, arg='s'): 
    434515    """ 
     
    456537        except TypeError: # len() of unsized object 
    457538            pass 
    458539    return singular_suffix 
     540pluralize.is_safe = False 
    459541 
    460542def phone2numeric(value): 
    461543    "Takes a phone number and converts it in to its numerical equivalent" 
    462544    from django.utils.text import phone2numeric 
    463545    return phone2numeric(value) 
     546phone2numeric.is_safe = True 
    464547 
    465548def pprint(value): 
    466549    "A wrapper around pprint.pprint -- for debugging, really" 
     
    469552        return pformat(value) 
    470553    except Exception, e: 
    471554        return "Error in formatting:%s" % e 
     555pprint.is_safe = True 
    472556 
    473557# Syntax: register.filter(name of filter, callback) 
    474558register.filter(add) 
     
    497581register.filter(ljust) 
    498582register.filter(lower) 
    499583register.filter(make_list) 
     584register.filter(noescape) 
    500585register.filter(phone2numeric) 
    501586register.filter(pluralize) 
    502587register.filter(pprint) 
  • django_autoescape/django/template/defaulttags.py

    old new  
    44from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END 
    55from django.template import get_library, Library, InvalidTemplateLibrary 
    66from django.conf import settings 
     7from django.utils.safestring import mark_safe 
    78import sys 
    89 
    910register = Library() 
    1011 
     12class AutoEscapeNode(Node): 
     13    def __init__(self, setting, nodelist): 
     14        self.setting, self.nodelist = setting, nodelist 
     15 
     16    def render(self, context): 
     17        old_setting = context.autoescape 
     18        context.autoescape = self.setting 
     19        output = self.nodelist.render(context) 
     20        context.autoescape = old_setting 
     21        if self.setting: 
     22            return mark_safe(output) 
     23        else: 
     24            return output 
     25 
    1126class CommentNode(Node): 
    1227    def render(self, context): 
    1328        return '' 
     
    318333        return str(int(round(ratio))) 
    319334 
    320335#@register.tag 
     336def autoescape(parser, token): 
     337    """ 
     338    Set autoescape behaviour for this block. Possible values are "on" and "off". 
     339    """ 
     340    unused, rest = token.contents.split(None, 1) 
     341    if rest not in ('on', 'off'): 
     342        raise TemplateSyntaxError("autoescape argument must be 'on' or 'off'") 
     343    setting = (rest == 'on') 
     344    nodelist = parser.parse(('endautoescape',)) 
     345    parser.delete_first_token() 
     346    return AutoEscapeNode(setting, nodelist) 
     347autoescape = register.tag(autoescape) 
     348 
     349#@register.tag 
    321350def comment(parser, token): 
    322351    """ 
    323352    Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` 
     
    417446    """ 
    418447    _, rest = token.contents.split(None, 1) 
    419448    filter_expr = parser.compile_filter("var|%s" % (rest)) 
     449    for func, unused in filter_expr.filters: 
     450        if func.__name__ == 'noescape': 
     451            raise TemplateSyntaxError('"filter noescape" is not permitted.  Use the "autoescape" tag instead.') 
    420452    nodelist = parser.parse(('endfilter',)) 
    421453    parser.delete_first_token() 
    422454    return FilterNode(filter_expr, nodelist) 
  • django_autoescape/django/template/__init__.py

    old new  
    5757import re 
    5858from inspect import getargspec 
    5959from django.conf import settings 
    60 from django.template.context import Context, RequestContext, ContextPopException 
     60from django.template.context import Context, SafeContext, RequestContext, ContextPopException 
    6161from django.utils.functional import curry 
    6262from django.utils.text import smart_split 
     63from django.utils.safestring import SafeData, mark_safe 
     64from django.utils.html import escape 
    6365 
    64 __all__ = ('Template', 'Context', 'RequestContext', 'compile_string') 
     66__all__ = ('Template', 'Context', 'SafeContext', 'RequestContext', 'compile_string') 
    6567 
    6668TOKEN_TEXT = 0 
    6769TOKEN_VAR = 1 
     
    558560                    arg_vals.append(arg) 
    559561                else: 
    560562                    arg_vals.append(resolve_variable(arg, context)) 
    561             obj = func(obj, *arg_vals) 
     563            if getattr(func, 'needs_autoescape', False): 
     564                new_obj = func(obj, autoescape = context.autoescape, *arg_vals) 
     565            else: 
     566                new_obj = func(obj, *arg_vals) 
     567            if getattr(func, 'is_safe', False) and isinstance(obj, SafeData): 
     568                obj = mark_safe(new_obj) 
     569            else: 
     570                obj = new_obj 
     571                 
    562572        return obj 
    563573 
    564574    def args_check(name, func, provided): 
     
    744754 
    745755    def render(self, context): 
    746756        output = self.filter_expression.resolve(context) 
    747         return self.encode_output(output) 
     757        encoded_output = self.encode_output(output) 
     758        if context.autoescape and not isinstance(encoded_output, SafeData): 
     759            return escape(encoded_output) 
     760        else: 
     761            return encoded_output 
    748762 
    749763class DebugVariableNode(VariableNode): 
    750764    def render(self, context): 
     
    754768            if not hasattr(e, 'source'): 
    755769                e.source = self.source 
    756770            raise 
    757         return self.encode_output(output) 
     771        encoded_output = self.encode_output(output) 
     772        if context.autoescape and not isinstance(encoded_output, SafeData): 
     773            return escape(encoded_output) 
     774        else: 
     775            return encoded_output 
    758776 
    759777def generic_tag_compiler(params, defaults, name, node_class, parser, token): 
    760778    "Returns a template.Node subclass." 
  • django_autoescape/django/utils/html.py

    old new  
    11"HTML utilities suitable for global use." 
    22 
    33import re, string 
     4from django.utils.safestring import SafeData 
    45 
    56# Configuration for urlize() function 
    67LEADING_PUNCTUATION  = ['(', '<', '&lt;'] 
     
    2728        html = str(html) 
    2829    return html.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;') 
    2930 
    30 def linebreaks(value): 
     31def conditional_escape(html): 
     32    "Similar to escape(), except that it does not operate on pre-escaped strings" 
     33    if isinstance(html, SafeData): 
     34        return html 
     35    else: 
     36        return escape(html) 
     37 
     38def linebreaks(value, autoescape = False): 
    3139    "Converts newlines into <p> and <br />s" 
    3240    value = re.sub(r'\r\n|\r|\n', '\n', value) # normalize newlines 
    3341    paras = re.split('\n{2,}', value) 
    34     paras = ['<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras] 
     42    if autoescape: 
     43        paras = ['<p>%s</p>' % escape(p.strip()).replace('\n', '<br />') for p in paras] 
     44    else: 
     45        paras = ['<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras] 
    3546    return '\n\n'.join(paras) 
    3647 
    3748def strip_tags(value): 
  • django_autoescape/django/utils/safestring.py

    old new  
     1""" 
     2Functions for working with "safe strings": strings that can be display safely 
     3without further modification in HTML. 
     4""" 
     5from django.utils.functional import curry 
     6 
     7class SafeData(object): 
     8    pass 
     9 
     10class SafeString(str, SafeData): 
     11    """ 
     12    A string subclass that has been specifically marked as "safe" for output 
     13    purposes. 
     14    """ 
     15    def __add__(self, rhs): 
     16        """ 
     17        Concatenating a safe string with another safe string or safe unicode 
     18        object is safe. Otherwise, the result is no longer safe. 
     19        """ 
     20        if isinstance(rhs, SafeUnicode): 
     21            return SafeUnicode(self + rhs) 
     22        elif isinstance(rhs, SafeString): 
     23            return SafeString(self, rhs) 
     24        else: 
     25            return super(SafeString, self).__add__(rhs) 
     26 
     27class SafeUnicode(unicode, SafeData): 
     28    """ 
     29    A unicode subclass that has been specifically marked as "safe" for output 
     30    purposes. 
     31    """ 
     32    def __add__(self, rhs): 
     33        """ 
     34        Concatenating a safe unicode object with another safe string or safe 
     35        unicode object is safe. Otherwise, the result is no longer safe. 
     36        """ 
     37        if isinstance(rhs, SafeData): 
     38            return SafeUnicode(self + rhs) 
     39        else: 
     40            return super(SafeUnicode, self).__add__(rhs) 
     41 
     42    def _proxy_method(self, *args, **kwargs): 
     43        """ 
     44        Wrap a call to a normal unicode method up so that we return safe 
     45        results. The method that is being wrapped is passed in the 'method' 
     46        argument. 
     47        """ 
     48        method = kwargs.pop('method') 
     49        data = method(self, *args, **kwargs) 
     50        if isinstance(data, str): 
     51            return SafeString(data) 
     52        else: 
     53            return SafeUnicode(data) 
     54 
     55    encode = curry(_proxy_method, method = unicode.encode) 
     56    decode = curry(_proxy_method, method = unicode.decode) 
     57 
     58 
     59def mark_safe(s): 
     60    """ 
     61    Explicitly mark a string as safe for output purposes. The returned object 
     62    can be used everywhere a string or unicode object is appropriate. 
     63 
     64    Can safely be called multiple times on a single string. 
     65    """ 
     66    if isinstance(s, SafeData): 
     67        return s 
     68    if isinstance(s, str): 
     69        return SafeString(s) 
     70    if isinstance(s, unicode): 
     71        return SafeUnicode(s) 
     72    raise TypeError, "Input must be str or unicode (got %s)" % type(s) 
     73 
  • django_autoescape/django/views/debug.py

    old new  
    11from django.conf import settings 
    2 from django.template import Template, Context, TemplateDoesNotExist 
     2from django.template import Template, Context, SafeContext, TemplateDoesNotExist 
    33from django.utils.html import escape 
    44from django.http import HttpResponseServerError, HttpResponseNotFound 
    55import os, re 
     
    118118            'lineno': '?', 
    119119        }] 
    120120    t = Template(TECHNICAL_500_TEMPLATE) 
    121     c = Context({ 
     121    c = SafeContext({ 
    122122        'exception_type': exc_type.__name__, 
    123123        'exception_value': exc_value, 
    124124        'frames': frames, 
     
    157157def empty_urlconf(request): 
    158158    "Create an empty URLconf 404 error response." 
    159159    t = Template(EMPTY_URLCONF_TEMPLATE) 
    160     c = Context({ 
     160    c = SafeContext({ 
    161161        'project_name': settings.SETTINGS_MODULE.split('.')[0] 
    162162    }) 
    163163    return HttpResponseNotFound(t.render(c), mimetype='text/html') 
     
    295295 
    296296<div id="summary"> 
    297297  <h1>{{ exception_type }} at {{ request.path }}</h1> 
    298   <h2>{{ exception_value|escape }}</h2> 
     298  <h2>{{ exception_value }}</h2> 
    299299  <table class="meta"> 
    300300    <tr> 
    301301      <th>Request Method:</th> 
     
    340340<div id="template"> 
    341341   <h2>Template error</h2> 
    342342   <p>In template <code>{{ template_info.name }}</code>, error at line <strong>{{ template_info.line }}</strong></p> 
    343    <h3>{{ template_info.message|escape }}</h3> 
     343   <h3>{{ template_info.message }}</h3> 
    344344   <table class="source{% if template_info.top %} cut-top{% endif %}{% ifnotequal template_info.bottom template_info.total %} cut-bottom{% endifnotequal %}"> 
    345345   {% for source_line in template_info.source_lines %} 
    346346   {% ifequal source_line.0 template_info.line %} 
     
    367367          {% if frame.context_line %} 
    368368            <div class="context" id="c{{ frame.id }}"> 
    369369              {% if frame.pre_context %} 
    370                 <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol> 
     370                <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line }}</li>{% endfor %}</ol> 
    371371              {% endif %} 
    372               <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line|escape }} <span>...</span></li></ol> 
     372              <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line }} <span>...</span></li></ol> 
    373373              {% if frame.post_context %} 
    374                 <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol> 
     374                <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line }}</li>{% endfor %}</ol> 
    375375              {% endif %} 
    376376            </div> 
    377377          {% endif %} 
     
    391391                {% for var in frame.vars|dictsort:"0" %} 
    392392                  <tr> 
    393393                    <td>{{ var.0 }}</td> 
    394                     <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     394                    <td class="code"><div>{{ var.1|pprint }}</div></td> 
    395395                  </tr> 
    396396                {% endfor %} 
    397397              </tbody> 
     
    411411{% for frame in frames %} 
    412412  File "{{ frame.filename }}" in {{ frame.function }}<br/> 
    413413  {% if frame.context_line %} 
    414     &nbsp;&nbsp;{{ frame.lineno }}. {{ frame.context_line|escape }}<br/> 
     414    &nbsp;&nbsp;{{ frame.lineno }}. {{ frame.context_line }}<br/> 
    415415  {% endif %} 
    416416{% endfor %}<br/> 
    417417&nbsp;&nbsp;{{ exception_type }} at {{ request.path }}<br/> 
    418 &nbsp;&nbsp;{{ exception_value|escape }}</code> 
     418&nbsp;&nbsp;{{ exception_value }}</code> 
    419419          </td> 
    420420        </tr> 
    421421      </tbody> 
     
    439439        {% for var in request.GET.items %} 
    440440          <tr> 
    441441            <td>{{ var.0 }}</td> 
    442             <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     442            <td class="code"><div>{{ var.1|pprint }}</div></td> 
    443443          </tr> 
    444444        {% endfor %} 
    445445      </tbody> 
     
    461461        {% for var in request.POST.items %} 
    462462          <tr> 
    463463            <td>{{ var.0 }}</td> 
    464             <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     464            <td class="code"><div>{{ var.1|pprint }}</div></td> 
    465465          </tr> 
    466466        {% endfor %} 
    467467      </tbody> 
     
    483483        {% for var in request.COOKIES.items %} 
    484484          <tr> 
    485485            <td>{{ var.0 }}</td> 
    486             <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     486            <td class="code"><div>{{ var.1|pprint }}</div></td> 
    487487          </tr> 
    488488        {% endfor %} 
    489489      </tbody> 
     
    504504      {% for var in request.META.items|dictsort:"0" %} 
    505505        <tr> 
    506506          <td>{{ var.0 }}</td> 
    507           <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     507          <td class="code"><div>{{ var.1|pprint }}</div></td> 
    508508        </tr> 
    509509      {% endfor %} 
    510510    </tbody> 
     
    523523      {% for var in settings.items|dictsort:"0" %} 
    524524        <tr> 
    525525          <td>{{ var.0 }}</td> 
    526           <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     526          <td class="code"><div>{{ var.1|pprint }}</div></td> 
    527527        </tr> 
    528528      {% endfor %} 
    529529    </tbody> 
     
    590590      </p> 
    591591      <ol> 
    592592        {% for pattern in urlpatterns %} 
    593           <li>{{ pattern|escape }}</li> 
     593          <li>{{ pattern }}</li> 
    594594        {% endfor %} 
    595595      </ol> 
    596596      <p>The current URL, <code>{{ request.path }}</code>, didn't match any of these.</p> 
    597597    {% else %} 
    598       <p>{{ reason|escape }}</p> 
     598      <p>{{ reason }}</p> 
    599599    {% endif %} 
    600600  </div> 
    601601 
  • django_autoescape/tests/othertests/autoescape.py

    old new  
     1from django.conf import settings 
     2 
     3if __name__ == '__main__': 
     4    # When running this file in isolation, we need to set up the configuration 
     5    # before importing 'template'. 
     6    settings.configure() 
     7 
     8import templates 
     9from django import template 
     10from django.template import loader 
     11from django.utils.translation import activate, deactivate, install 
     12from django.utils.tzinfo import LocalTimezone 
     13from django.utils.safestring import mark_safe 
     14from datetime import datetime, timedelta 
     15import traceback 
     16 
     17# We want to check all the normal template tests against SafeContext by 
     18# default. We just update results that would change with autoescaping and add 
     19# in new tests. 
     20TEMPLATE_TESTS = templates.TEMPLATE_TESTS.copy() 
     21 
     22# SYNTAX -- 
     23# 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) 
     24TEMPLATE_TESTS.update({ 
     25 
     26    ### BASIC SYNTAX ########################################################## 
     27 
     28    # Escaped string as argument (this replaces the non-escaped version) 
     29    'basic-syntax30': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote&quot; hah'), 
     30 
     31    # Autoescaping is applied by default 
     32    'autoescape-basic01': ("{{ first }}", {"first": "<b>first</b>"}, "&lt;b&gt;first&lt;/b&gt;"), 
     33 
     34    # Strings (ASCII or unicode) already marked as "safe" are not auto-escaped 
     35    'autoescape-basic02': ("{{ first }}", {"first": mark_safe("<b>first</b>")}, "<b>first</b>"), 
     36    'autoescape-basic03': ("{{ first }}", {"first": mark_safe(u"<b>Apple</b>")}, u"<b>Apple</b>"), 
     37 
     38 
     39    ### AUTOESCAPE TAG ######################################################## 
     40    'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"), 
     41    'autoescape-tag02': ("{% autoescape off %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"), 
     42    'autoescape-tag03': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "&lt;b&gt;hello&lt;/b&gt;"), 
     43 
     44    ### FILTER TAG ############################################################ 
     45 
     46    # The "noescape" filter cannot work due to internal implementation details 
     47    # (fortunately, it is equivalent to using the autoescape tag in these 
     48    # cases). 
     49    'autoescape-filtertag01': ("{{ first }}{% filter noescape %}{{ first }} x<y{% endfilter %}", {"first": "<a>"}, template.TemplateSyntaxError), 
     50 
     51    ### FILTER TESTS ########################################################## 
     52 
     53    'ae-filter-addslash01': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"&lt;a&gt;\&#39; <a>\'"), 
     54 
     55    'ae-filter-capfirst01': ("{{ a|capfirst }} {{ b|capfirst }}", {"a": "fred>", "b": mark_safe("fred&gt;")}, "Fred&gt; Fred&gt;"), 
     56 
     57    # Note that applying fix_ampsersands in autoescape mode leads to double 
     58    # escaping. 
     59    'ae-filter-fix_ampersands01': ("{{ a|fix_ampersands }} {{ b|fix_ampersands }}", {"a": "a&b", "b": mark_safe("a&b")}, "a&amp;amp;b a&amp;b"), 
     60 
     61    'ae-filter-floatformat01': ("{{ a|floatformat }} {{ b|floatformat }}", {"a": "1.42", "b": mark_safe("1.42")}, "1.4 1.4"), 
     62 
     63    # The contents of "linenumbers" is escaped according to the current 
     64    # autoescape setting. 
     65    'ae-filter-linenumbers01': ("{{ a|linenumbers }} {{ b|linenumbers }}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n&lt;two&gt;\nthree")}, "1. one\n2. &lt;two&gt;\n3. three 1. one\n2. &lt;two&gt;\n3. three"), 
     66    'ae-filter-linenumbers02': ("{% autoescape off %}{{ a|linenumbers }} {{ b|linenumbers }}{% endautoescape %}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n&lt;two&gt;\nthree")}, "1. one\n2. <two>\n3. three 1. one\n2. &lt;two&gt;\n3. three"), 
     67 
     68    'ae-filter-lower01': ("{{ a|lower }} {{ b|lower }}", {"a": "Apple & banana", "b": mark_safe("Apple &amp; banana")}, "apple &amp; banana apple &amp; banana"), 
     69 
     70    # The make_list filter can destroy # existing encoding, so the results are 
     71    # escaped. 
     72    'ae-filter-make_list01': ("{{ a|make_list }}", {"a": mark_safe("&")}, "[&#39;&amp;&#39;]"), 
     73    'ae-filter-make_list02': ('{{ a|make_list|stringformat:"s"|noescape }}', {"a": mark_safe("&")}, "['&']"), 
     74 
     75    # Running slugify on a pre-escaped string leads to odd behaviour, but the 
     76    # result is still safe. 
     77    'ae-filter-slugify01': ("{{ a|slugify }} {{ b|slugify }}", {"a": "a & b", "b": mark_safe("a &amp; b")}, "a-b a-amp-b"), 
     78     
     79    # Notice that escaping is applied *after* any filters, so the string 
     80    # formatting here only needs to deal with pre-escaped characters. 
     81    'ae-filter-stringformat01': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, ".  a&lt;b. .  a<b."), 
     82 
     83    # XXX No test for "title" filter; needs an actual object. 
     84     
     85    'ae-filter-truncatewords01': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', {"a": "alpha & bravo", "b": mark_safe("alpha &amp; bravo")}, "alpha &amp; ... alpha &amp; ..."), 
     86 
     87    # The "upper" filter messes up entities (which are case-sensitive), so it's 
     88    # not safe for non-escaping purposes. 
     89    'ae-filter-upper01': ('{{ a|upper }} {{ b|upper }}', {"a": "a & b", "b": mark_safe("a &amp; b")}, "A &amp; B A &amp;AMP; B"), 
     90 
     91    'ae-filter-urlize01': ('{{ a|urlize }} {{ b|urlize }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, '<a href="http://example.com/x=&y=" rel="nofollow">http://example.com/x=&y=</a> <a href="http://example.com?x=&y=" rel="nofollow">http://example.com?x=&y=</a>'), 
     92    'ae-fil