Improved reporting of deletion consequences. Removed vestigial "Advanced" menu.
[philo.git] / validators.py
index bc41d02..5ae9409 100644 (file)
-from django.core.exceptions import ValidationError
 from django.utils.translation import ugettext_lazy as _
+from django.core.validators import RegexValidator
+from django.core.exceptions import ValidationError
+from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError
+from django.utils import simplejson as json
+from django.utils.html import escape, mark_safe
+import re
+from philo.utils import LOADED_TEMPLATE_ATTR
 
 
-class TreeParentValidator(object):
-       """
-       constructor takes instance and parent_attr, where instance is the model
-       being validated and parent_attr is where to look on that parent for the
-       comparison.
-       """
-       #message = _("A tree element can't be its own parent.")
-       code = 'invalid'
-       
-       def __init__(self, instance, parent_attr=None, message=None, code=None):
-               self.instance = instance
-               self.parent_attr = parent_attr
-               self.static_message = message
-               if code is not None:
-                       self.code = code
-       
-       def __call__(self, value):
-               """
-               Validates that the self.instance is not found in the parent tree of
-               the node given as value.
-               """
-               parent = value
+INSECURE_TAGS = (
+       'load',
+       'extends',
+       'include',
+       'debug',
+)
+
+
+class RedirectValidator(RegexValidator):
+       """Based loosely on the URLValidator, but no option to verify_exists"""
+       regex = re.compile(
+               r'^(?:https?://' # http:// or https://
+               r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
+               r'localhost|' #localhost...
+               r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+               r'(?::\d+)?' # optional port
+               r'(?:/?|[/?#]?\S+)|'
+               r'[^?#\s]\S*)$',
+               re.IGNORECASE)
+       message = _(u'Enter a valid absolute or relative redirect target')
+
+
+class URLLinkValidator(RegexValidator):
+       """Based loosely on the URLValidator, but no option to verify_exists"""
+       regex = re.compile(
+               r'^(?:https?://' # http:// or https://
+               r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain...
+               r'localhost|' #localhost...
+               r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+               r'(?::\d+)?' # optional port
+               r'|)' # also allow internal links
+               r'(?:/?|[/?#]?\S+)$', re.IGNORECASE)
+       message = _(u'Enter a valid absolute or relative redirect target')
+
+
+def json_validator(value):
+       try:
+               json.loads(value)
+       except Exception, e:
+               raise ValidationError(u'JSON decode error: %s' % e)
+
+
+class TemplateValidationParser(Parser):
+       def __init__(self, tokens, allow=None, disallow=None, secure=True):
+               super(TemplateValidationParser, self).__init__(tokens)
                
-               while parent:
-                       comparison=self.get_comparison(parent)
-                       if comparison == self.instance:
-                               # using (self.message, code=self.code) results in the admin interface
-                               # screwing with the error message and making it be 'Enter a valid value'
-                               raise ValidationError(self.message)
-                       parent=parent.parent
-       
-       def get_comparison(self, parent):
-               if self.parent_attr and hasattr(parent, self.parent_attr):
-                       return getattr(parent, self.parent_attr)
+               allow, disallow = set(allow or []), set(disallow or [])
                
-               return parent
-       
-       def get_message(self):
-               return self.static_message or _(u"A %s can't be its own parent." % self.instance.__class__.__name__)
-       message = property(get_message)
+               if secure:
+                       disallow |= set(INSECURE_TAGS)
+               
+               self.allow, self.disallow, self.secure = allow, disallow, secure
        
-class TreePositionValidator(object):
-       code = 'invalid'
+       def parse(self, parse_until=None):
+               if parse_until is None:
+                       parse_until = []
+               
+               nodelist = self.create_nodelist()
+               while self.tokens:
+                       token = self.next_token()
+                       # We only need to parse var and block tokens.
+                       if token.token_type == TOKEN_VAR:
+                               if not token.contents:
+                                       self.empty_variable(token)
+                               
+                               filter_expression = self.compile_filter(token.contents)
+                               var_node = self.create_variable_node(filter_expression)
+                               self.extend_nodelist(nodelist, var_node,token)
+                       elif token.token_type == TOKEN_BLOCK:
+                               if token.contents in parse_until:
+                                       # put token back on token list so calling code knows why it terminated
+                                       self.prepend_token(token)
+                                       return nodelist
+                               
+                               try:
+                                       command = token.contents.split()[0]
+                               except IndexError:
+                                       self.empty_block_tag(token)
+                               
+                               if (self.allow and command not in self.allow) or (self.disallow and command in self.disallow):
+                                       self.disallowed_tag(command)
+                               
+                               self.enter_command(command, token)
+                               
+                               try:
+                                       compile_func = self.tags[command]
+                               except KeyError:
+                                       self.invalid_block_tag(token, command, parse_until)
+                               
+                               try:
+                                       compiled_result = compile_func(self, token)
+                               except TemplateSyntaxError, e:
+                                       if not self.compile_function_error(token, e):
+                                               raise
+                               
+                               self.extend_nodelist(nodelist, compiled_result, token)
+                               self.exit_command()
+               
+               if parse_until:
+                       self.unclosed_block_tag(parse_until)
+               
+               return nodelist
        
-       def __init__(self, parent, slug, obj_class, message=None, code=None):
-               self.parent = parent
-               self.slug = slug
-               self.obj_class = obj_class
-               self.static_message = message
-                       
-               if code is not None:
-                       self.code = code
+       def disallowed_tag(self, command):
+               if self.secure and command in INSECURE_TAGS:
+                       raise ValidationError('Tag "%s" is not permitted for security reasons.' % command)
+               raise ValidationError('Tag "%s" is not permitted here.' % command)
+
+
+def linebreak_iter(template_source):
+       # Cribbed from django/views/debug.py
+       yield 0
+       p = template_source.find('\n')
+       while p >= 0:
+               yield p+1
+               p = template_source.find('\n', p+1)
+       yield len(template_source) + 1
+
+
+class TemplateValidator(object): 
+       def __init__(self, allow=None, disallow=None, secure=True):
+               self.allow = allow
+               self.disallow = disallow
+               self.secure = secure
        
        def __call__(self, value):
-               """
-               Validates that there is no obj of obj_class with the same position
-               as the compared obj (value) but a different id.
-               """
-               if not isinstance(value, self.obj_class):
-                       raise ValidationError(_(u"The value must be an instance of %s." % self.obj_class.__name__))
-               
                try:
-                       obj = self.obj_class.objects.get(slug=self.slug, parent=self.parent)
-                       
-                       if obj.id != value.id:
-                               raise ValidationError(self.message)
-                               
-               except self.obj_class.DoesNotExist:
-                       pass
+                       self.validate_template(value)
+               except ValidationError:
+                       raise
+               except Exception, e:
+                       if hasattr(e, 'source') and isinstance(e, TemplateSyntaxError):
+                               origin, (start, end) = e.source
+                               template_source = origin.reload()
+                               upto = 0
+                               for num, next in enumerate(linebreak_iter(template_source)):
+                                       if start >= upto and end <= next:
+                                               raise ValidationError(mark_safe("Template code invalid: \"%s\" (%s:%d).<br />%s" % (escape(template_source[start:end]), origin.loadname, num, e)))
+                                       upto = next
+                       raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
        
-       def get_message(self):
-               return self.static_message or _(u"A %s with that path (parent and slug) already exists." % self.obj_class.__name__)
-       message = property(get_message)
+       def validate_template(self, template_string):
+               # We want to tokenize like normal, then use a custom parser.
+               lexer = Lexer(template_string, None)
+               tokens = lexer.tokenize()
+               parser = TemplateValidationParser(tokens, self.allow, self.disallow, self.secure)
+               
+               for node in parser.parse():
+                       template = getattr(node, LOADED_TEMPLATE_ATTR, None)
\ No newline at end of file