From 3105e851ce4d7d426b3cffbd601c5b4e12075b41 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Thu, 30 Sep 2010 18:15:52 -0400 Subject: [PATCH] Added template field to master branch. Added monkeypatch for telling if a node includes a template and what that template is - eases container fetching. Added template validator which can allow/disallow tags by using a custom parser. Requires testing. --- models/fields.py | 17 +++++++- models/pages.py | 37 +++++++++++------ validators.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 15 deletions(-) diff --git a/models/fields.py b/models/fields.py index 50df799..79b31b3 100644 --- a/models/fields.py +++ b/models/fields.py @@ -4,6 +4,7 @@ from django.core.exceptions import FieldError from django.utils.text import capfirst from philo.models.base import Entity from philo.signals import entity_class_prepared +from philo.validators import TemplateValidator __all__ = ('AttributeField', 'RelationshipField') @@ -142,4 +143,18 @@ class RelationshipField(EntityProxyField): def value_from_object(self, obj): relobj = super(RelationshipField, self).value_from_object(obj) - return getattr(relobj, 'pk', None) \ No newline at end of file + return getattr(relobj, 'pk', None) + + +class TemplateField(models.TextField): + def __init__(self, allow=None, disallow=None, *args, **kwargs): + super(TemplateField, self).__init__(*args, **kwargs) + self.validators.append(TemplateValidator(allow, disallow)) + + +try: + from south.modelsinspector import add_introspection_rules +except ImportError: + pass +else: + add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"]) \ No newline at end of file diff --git a/models/pages.py b/models/pages.py index e000b1b..45ffe64 100644 --- a/models/pages.py +++ b/models/pages.py @@ -8,15 +8,32 @@ from django.template import Template as DjangoTemplate from django.template import TemplateDoesNotExist from django.template import Context, RequestContext from django.template.loader import get_template -from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode +from django.template.loader_tags import ExtendsNode, ConstantIncludeNode from django.http import HttpResponse from philo.models.base import TreeModel, register_value_model from philo.models.nodes import View from philo.utils import fattr from philo.templatetags.containers import ContainerNode +from philo.validators import LOADED_TEMPLATE_ATTR from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string +BLANK_CONTEXT = Context() + + +def get_extended(self): + return self.get_parent(BLANK_CONTEXT) + + +def get_included(self): + return self.template + + +# We ignore the IncludeNode because it will never work in a blank context. +setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended)) +setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included)) + + class Template(TreeModel): name = models.CharField(max_length=255) documentation = models.TextField(null=True, blank=True) @@ -48,20 +65,14 @@ class Template(TreeModel): for nodelist_name in node.child_nodelists: if hasattr(node, nodelist_name): nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name))) + + # _philo_additional_template is a property philo provides on all nodes that require it + # and which it monkeypatches onto the relevant default nodes. + if hasattr(node, LOADED_TEMPLATE_ATTR) and getattr(node, LOADED_TEMPLATE_ATTR): + nodes.extend(container_nodes(LOADED_TEMPLATE_ATTR)) + if isinstance(node, ContainerNode): nodes.append(node) - elif isinstance(node, ExtendsNode): - extended_template = node.get_parent(Context()) - if extended_template: - nodes.extend(container_nodes(extended_template)) - elif isinstance(node, ConstantIncludeNode): - included_template = node.template - if included_template: - nodes.extend(container_nodes(included_template)) - elif isinstance(node, IncludeNode): - included_template = get_template(node.template_name.resolve(Context())) - if included_template: - nodes.extend(container_nodes(included_template)) except: raise # fail for this node return nodes diff --git a/validators.py b/validators.py index e4c32d0..106db8b 100644 --- a/validators.py +++ b/validators.py @@ -5,6 +5,14 @@ from django.utils import simplejson as json import re +LOADED_TEMPLATE_ATTR = '_philo_loaded_template' +INSECURE_TAGS = ( + 'load', + 'extends', + 'include', +) + + class RedirectValidator(RegexValidator): """Based loosely on the URLValidator, but no option to verify_exists""" regex = re.compile( @@ -36,4 +44,96 @@ def json_validator(value): try: json.loads(value) except: - raise ValidationError(u'\'%s\' is not valid JSON' % value) \ No newline at end of file + raise ValidationError(u'\'%s\' is not valid JSON' % value) + + +from django.template import Template, Parser, Lexer, TOKEN_BLOCK + + +class TemplateValidationParser(Parser): + def __init__(self, tokens, allow=None, disallow=None, secure=True): + super(TemplateValidationParser, self).__init__(tokens) + + allow, disallow = set(allow or []), set(disallow or []) + + if secure: + disallow |= set(INSECURE_TAGS) + + self.allow, self.disallow = allow, disallow + + 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 disallowed_tag(self, command): + raise ValidationError("Tag not allowed: %s" % command) + + +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): + try: + self.validate_template(value) + except ValidationError: + raise + except Exception, e: + raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e)) + + 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 -- 2.20.1