Merge branch 'release/0.9.1'
[philo.git] / philo / validators.py
1 import re
2
3 from django.core.exceptions import ValidationError
4 from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError
5 from django.utils import simplejson as json
6 from django.utils.html import escape, mark_safe
7 from django.utils.translation import ugettext_lazy as _
8
9 from philo.utils.templates import LOADED_TEMPLATE_ATTR
10
11
12 #: Tags which are considered insecure and are therefore always disallowed by secure :class:`TemplateValidator` instances.
13 INSECURE_TAGS = (
14         'load',
15         'extends',
16         'include',
17         'debug',
18 )
19
20
21 def json_validator(value):
22         """Validates whether ``value`` is a valid json string."""
23         try:
24                 json.loads(value)
25         except Exception, e:
26                 raise ValidationError(u'JSON decode error: %s' % e)
27
28
29 class TemplateValidationParser(Parser):
30         def __init__(self, tokens, allow=None, disallow=None, secure=True):
31                 super(TemplateValidationParser, self).__init__(tokens)
32                 
33                 allow, disallow = set(allow or []), set(disallow or [])
34                 
35                 if secure:
36                         disallow |= set(INSECURE_TAGS)
37                 
38                 self.allow, self.disallow, self.secure = allow, disallow, secure
39         
40         def parse(self, parse_until=None):
41                 if parse_until is None:
42                         parse_until = []
43                 
44                 nodelist = self.create_nodelist()
45                 while self.tokens:
46                         token = self.next_token()
47                         # We only need to parse var and block tokens.
48                         if token.token_type == TOKEN_VAR:
49                                 if not token.contents:
50                                         self.empty_variable(token)
51                                 
52                                 filter_expression = self.compile_filter(token.contents)
53                                 var_node = self.create_variable_node(filter_expression)
54                                 self.extend_nodelist(nodelist, var_node,token)
55                         elif token.token_type == TOKEN_BLOCK:
56                                 if token.contents in parse_until:
57                                         # put token back on token list so calling code knows why it terminated
58                                         self.prepend_token(token)
59                                         return nodelist
60                                 
61                                 try:
62                                         command = token.contents.split()[0]
63                                 except IndexError:
64                                         self.empty_block_tag(token)
65                                 
66                                 if (self.allow and command not in self.allow) or (self.disallow and command in self.disallow):
67                                         self.disallowed_tag(command)
68                                 
69                                 self.enter_command(command, token)
70                                 
71                                 try:
72                                         compile_func = self.tags[command]
73                                 except KeyError:
74                                         self.invalid_block_tag(token, command, parse_until)
75                                 
76                                 try:
77                                         compiled_result = compile_func(self, token)
78                                 except TemplateSyntaxError, e:
79                                         if not self.compile_function_error(token, e):
80                                                 raise
81                                 
82                                 self.extend_nodelist(nodelist, compiled_result, token)
83                                 self.exit_command()
84                 
85                 if parse_until:
86                         self.unclosed_block_tag(parse_until)
87                 
88                 return nodelist
89         
90         def disallowed_tag(self, command):
91                 if self.secure and command in INSECURE_TAGS:
92                         raise ValidationError('Tag "%s" is not permitted for security reasons.' % command)
93                 raise ValidationError('Tag "%s" is not permitted here.' % command)
94
95
96 def linebreak_iter(template_source):
97         # Cribbed from django/views/debug.py:18
98         yield 0
99         p = template_source.find('\n')
100         while p >= 0:
101                 yield p+1
102                 p = template_source.find('\n', p+1)
103         yield len(template_source) + 1
104
105
106 class TemplateValidator(object): 
107         """
108         Validates whether a string represents valid Django template code.
109         
110         :param allow: ``None`` or an iterable of tag names which are explicitly allowed. If provided, tags whose names are not in the iterable will cause a ValidationError to be raised if they are used in the template code.
111         :param disallow: ``None`` or an iterable of tag names which are explicitly allowed. If provided, tags whose names are in the iterable will cause a ValidationError to be raised if they are used in the template code. If a tag's name is in ``allow`` and ``disallow``, it will be disallowed.
112         :param secure: If the validator is set to secure, it will automatically disallow the tag names listed in :const:`INSECURE_TAGS`. Defaults to ``True``.
113         
114         """
115         def __init__(self, allow=None, disallow=None, secure=True):
116                 self.allow = allow
117                 self.disallow = disallow
118                 self.secure = secure
119         
120         def __call__(self, value):
121                 try:
122                         self.validate_template(value)
123                 except ValidationError:
124                         raise
125                 except Exception, e:
126                         if hasattr(e, 'source') and isinstance(e, TemplateSyntaxError):
127                                 origin, (start, end) = e.source
128                                 template_source = origin.reload()
129                                 upto = 0
130                                 for num, next in enumerate(linebreak_iter(template_source)):
131                                         if start >= upto and end <= next:
132                                                 raise ValidationError(mark_safe("Template code invalid: \"%s\" (%s:%d).<br />%s" % (escape(template_source[start:end]), origin.loadname, num, e)))
133                                         upto = next
134                         raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
135         
136         def validate_template(self, template_string):
137                 # We want to tokenize like normal, then use a custom parser.
138                 lexer = Lexer(template_string, None)
139                 tokens = lexer.tokenize()
140                 parser = TemplateValidationParser(tokens, self.allow, self.disallow, self.secure)
141                 
142                 for node in parser.parse():
143                         template = getattr(node, LOADED_TEMPLATE_ATTR, None)