Moved Node and View documentation from rst files to the source file.
[philo.git] / philo / validators.py
1 import re
2
3 from django.core.exceptions import ValidationError
4 from django.core.validators import RegexValidator
5 from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError
6 from django.utils import simplejson as json
7 from django.utils.html import escape, mark_safe
8 from django.utils.translation import ugettext_lazy as _
9
10 from philo.utils import LOADED_TEMPLATE_ATTR
11
12
13 INSECURE_TAGS = (
14         'load',
15         'extends',
16         'include',
17         'debug',
18 )
19
20
21 class RedirectValidator(RegexValidator):
22         """Based loosely on the URLValidator, but no option to verify_exists"""
23         regex = re.compile(
24                 r'^(?:https?://' # http:// or https://
25                 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
26                 r'localhost|' #localhost...
27                 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
28                 r'(?::\d+)?' # optional port
29                 r'(?:/?|[/?#]?\S+)|'
30                 r'[^?#\s]\S*)$',
31                 re.IGNORECASE)
32         message = _(u'Enter a valid absolute or relative redirect target')
33
34
35 class URLLinkValidator(RegexValidator):
36         """Based loosely on the URLValidator, but no option to verify_exists"""
37         regex = re.compile(
38                 r'^(?:https?://' # http:// or https://
39                 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
40                 r'localhost|' #localhost...
41                 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
42                 r'(?::\d+)?' # optional port
43                 r'|)' # also allow internal links
44                 r'(?:/?|[/?#]?\S+)$', re.IGNORECASE)
45         message = _(u'Enter a valid absolute or relative redirect target')
46
47
48 def json_validator(value):
49         try:
50                 json.loads(value)
51         except Exception, e:
52                 raise ValidationError(u'JSON decode error: %s' % e)
53
54
55 class TemplateValidationParser(Parser):
56         def __init__(self, tokens, allow=None, disallow=None, secure=True):
57                 super(TemplateValidationParser, self).__init__(tokens)
58                 
59                 allow, disallow = set(allow or []), set(disallow or [])
60                 
61                 if secure:
62                         disallow |= set(INSECURE_TAGS)
63                 
64                 self.allow, self.disallow, self.secure = allow, disallow, secure
65         
66         def parse(self, parse_until=None):
67                 if parse_until is None:
68                         parse_until = []
69                 
70                 nodelist = self.create_nodelist()
71                 while self.tokens:
72                         token = self.next_token()
73                         # We only need to parse var and block tokens.
74                         if token.token_type == TOKEN_VAR:
75                                 if not token.contents:
76                                         self.empty_variable(token)
77                                 
78                                 filter_expression = self.compile_filter(token.contents)
79                                 var_node = self.create_variable_node(filter_expression)
80                                 self.extend_nodelist(nodelist, var_node,token)
81                         elif token.token_type == TOKEN_BLOCK:
82                                 if token.contents in parse_until:
83                                         # put token back on token list so calling code knows why it terminated
84                                         self.prepend_token(token)
85                                         return nodelist
86                                 
87                                 try:
88                                         command = token.contents.split()[0]
89                                 except IndexError:
90                                         self.empty_block_tag(token)
91                                 
92                                 if (self.allow and command not in self.allow) or (self.disallow and command in self.disallow):
93                                         self.disallowed_tag(command)
94                                 
95                                 self.enter_command(command, token)
96                                 
97                                 try:
98                                         compile_func = self.tags[command]
99                                 except KeyError:
100                                         self.invalid_block_tag(token, command, parse_until)
101                                 
102                                 try:
103                                         compiled_result = compile_func(self, token)
104                                 except TemplateSyntaxError, e:
105                                         if not self.compile_function_error(token, e):
106                                                 raise
107                                 
108                                 self.extend_nodelist(nodelist, compiled_result, token)
109                                 self.exit_command()
110                 
111                 if parse_until:
112                         self.unclosed_block_tag(parse_until)
113                 
114                 return nodelist
115         
116         def disallowed_tag(self, command):
117                 if self.secure and command in INSECURE_TAGS:
118                         raise ValidationError('Tag "%s" is not permitted for security reasons.' % command)
119                 raise ValidationError('Tag "%s" is not permitted here.' % command)
120
121
122 def linebreak_iter(template_source):
123         # Cribbed from django/views/debug.py:18
124         yield 0
125         p = template_source.find('\n')
126         while p >= 0:
127                 yield p+1
128                 p = template_source.find('\n', p+1)
129         yield len(template_source) + 1
130
131
132 class TemplateValidator(object): 
133         def __init__(self, allow=None, disallow=None, secure=True):
134                 self.allow = allow
135                 self.disallow = disallow
136                 self.secure = secure
137         
138         def __call__(self, value):
139                 try:
140                         self.validate_template(value)
141                 except ValidationError:
142                         raise
143                 except Exception, e:
144                         if hasattr(e, 'source') and isinstance(e, TemplateSyntaxError):
145                                 origin, (start, end) = e.source
146                                 template_source = origin.reload()
147                                 upto = 0
148                                 for num, next in enumerate(linebreak_iter(template_source)):
149                                         if start >= upto and end <= next:
150                                                 raise ValidationError(mark_safe("Template code invalid: \"%s\" (%s:%d).<br />%s" % (escape(template_source[start:end]), origin.loadname, num, e)))
151                                         upto = next
152                         raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
153         
154         def validate_template(self, template_string):
155                 # We want to tokenize like normal, then use a custom parser.
156                 lexer = Lexer(template_string, None)
157                 tokens = lexer.tokenize()
158                 parser = TemplateValidationParser(tokens, self.allow, self.disallow, self.secure)
159                 
160                 for node in parser.parse():
161                         template = getattr(node, LOADED_TEMPLATE_ATTR, None)