From: Armin Ronacher Date: Mon, 14 Apr 2008 20:53:58 +0000 (+0200) Subject: added sandbox and exchageable undefined objects X-Git-Tag: 2.0rc1~170 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=c63243e036012c4ef6a9c04329e2e45e0a21bd31;p=jinja2.git added sandbox and exchageable undefined objects --HG-- branch : trunk --- diff --git a/jinja2/__init__.py b/jinja2/__init__.py index b54c504..dcebefc 100644 --- a/jinja2/__init__.py +++ b/jinja2/__init__.py @@ -57,3 +57,4 @@ :license: BSD, see LICENSE for more details. """ from jinja2.environment import Environment +from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined diff --git a/jinja2/compiler.py b/jinja2/compiler.py index 4ddacd9..e299c6f 100644 --- a/jinja2/compiler.py +++ b/jinja2/compiler.py @@ -457,8 +457,8 @@ class CodeGenerator(NodeVisitor): self.writeline('def root(globals, environment=environment' ', standalone=False):', extra=1) self.indent() - self.writeline('context = TemplateContext(globals, %r, blocks' - ', standalone)' % self.filename) + self.writeline('context = TemplateContext(environment, globals, %r, ' + 'blocks, standalone)' % self.filename) if have_extends: self.writeline('parent_root = None') self.outdent() @@ -613,7 +613,7 @@ class CodeGenerator(NodeVisitor): # the expression pointing to the parent loop. We make the # undefined a bit more debug friendly at the same time. parent_loop = 'loop' in aliases and aliases['loop'] \ - or "Undefined('loop', extra=%r)" % \ + or "environment.undefined('loop', extra=%r)" % \ 'the filter section of a loop as well as the ' \ 'else block doesn\'t have access to the special ' \ "'loop' variable of the current loop. Because " \ @@ -691,8 +691,8 @@ class CodeGenerator(NodeVisitor): arg_tuple = ', '.join(repr(x.name) for x in node.args) if len(node.args) == 1: arg_tuple += ',' - self.write('l_%s = Macro(macro, %r, (%s), (' % (node.name, node.name, - arg_tuple)) + self.write('l_%s = Macro(environment, macro, %r, (%s), (' % + (node.name, node.name, arg_tuple)) for arg in node.defaults: self.visit(arg, macro_frame) self.write(', ') @@ -715,7 +715,8 @@ class CodeGenerator(NodeVisitor): arg_tuple = ', '.join(repr(x.name) for x in node.args) if len(node.args) == 1: arg_tuple += ',' - self.writeline('caller = Macro(call, None, (%s), (' % arg_tuple) + self.writeline('caller = Macro(environment, call, None, (%s), (' % + arg_tuple) for arg in node.defaults: self.visit(arg) self.write(', ') @@ -960,7 +961,7 @@ class CodeGenerator(NodeVisitor): self.visit(node.node, frame) self.write('[%s]' % const) return - self.write('subscribe(') + self.write('environment.subscribe(') self.visit(node.node, frame) self.write(', ') if have_const: @@ -1019,8 +1020,10 @@ class CodeGenerator(NodeVisitor): self.write(')') def visit_Call(self, node, frame, extra_kwargs=None): + if self.environment.sandboxed: + self.write('environment.call(') self.visit(node.node, frame) - self.write('(') + self.write(self.environment.sandboxed and ', ' or '(') self.signature(node, frame, False, extra_kwargs) self.write(')') diff --git a/jinja2/defaults.py b/jinja2/defaults.py index 4ad0e3a..7959180 100644 --- a/jinja2/defaults.py +++ b/jinja2/defaults.py @@ -1,18 +1,16 @@ # -*- coding: utf-8 -*- """ - jinja.defaults - ~~~~~~~~~~~~~~ + jinja2.defaults + ~~~~~~~~~~~~~~~ Jinja default filters and tags. - :copyright: 2007 by Armin Ronacher. + :copyright: 2007-2008 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ from jinja2.filters import FILTERS as DEFAULT_FILTERS from jinja.tests import TESTS as DEFAULT_TESTS + DEFAULT_NAMESPACE = { 'range': xrange } - - -__all__ = ['DEFAULT_FILTERS', 'DEFAULT_TESTS', 'DEFAULT_NAMESPACE'] diff --git a/jinja2/environment.py b/jinja2/environment.py index 9d2371d..33e8f76 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -12,6 +12,7 @@ from jinja2.lexer import Lexer from jinja2.parser import Parser from jinja2.optimizer import optimize from jinja2.compiler import generate +from jinja2.runtime import Undefined from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE @@ -23,6 +24,11 @@ class Environment(object): globals and others. """ + #: if this environment is sandboxed. Modifying this variable won't make + #: the environment sandboxed though. For a real sandboxed environment + #: have a look at jinja2.sandbox + sandboxed = False + def __init__(self, block_start_string='{%', block_end_string='%}', @@ -33,6 +39,7 @@ class Environment(object): line_statement_prefix=None, trim_blocks=False, optimized=True, + undefined=Undefined, loader=None): """Here the possible initialization parameters: @@ -55,9 +62,13 @@ class Environment(object): variable tag!). Defaults to ``False``. `optimized` should the optimizer be enabled? Default is ``True``. + `undefined` a subclass of `Undefined` that is used to + represent undefined variables. `loader` the loader which should be used. ========================= ============================================ """ + assert issubclass(undefined, Undefined), 'undefined must be ' \ + 'a subclass of undefined because filters depend on it.' # lexer / parser information self.block_start_string = block_start_string @@ -68,6 +79,7 @@ class Environment(object): self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.trim_blocks = trim_blocks + self.undefined = undefined self.optimized = optimized # defaults @@ -87,6 +99,16 @@ class Environment(object): # create lexer self.lexer = Lexer(self) + def subscribe(self, obj, argument): + """Get an item or attribute of an object.""" + try: + return getattr(obj, str(argument)) + except (AttributeError, UnicodeError): + try: + return obj[argument] + except (TypeError, LookupError): + return self.undefined(obj, argument) + def parse(self, source, filename=None): """Parse the sourcecode and return the abstract syntax tree. This tree of nodes is used by the compiler to convert the template into @@ -186,3 +208,56 @@ class Template(object): # skip the first item which is a reference to the stream gen.next() return gen + + def __repr__(self): + return '<%s %r>' % ( + self.__class__.__name__, + self.name + ) + + +class TemplateStream(object): + """Wraps a genererator for outputing template streams.""" + + def __init__(self, gen): + self._gen = gen + self._next = gen.next + self.buffered = False + + def disable_buffering(self): + """Disable the output buffering.""" + self._next = self._gen.next + self.buffered = False + + def enable_buffering(self, size=5): + """Enable buffering. Buffer `size` items before yielding them.""" + if size <= 1: + raise ValueError('buffer size too small') + self.buffered = True + + def buffering_next(): + buf = [] + c_size = 0 + push = buf.append + next = self._gen.next + + try: + while 1: + item = next() + if item: + push(item) + c_size += 1 + if c_size >= size: + raise StopIteration() + except StopIteration: + if not c_size: + raise + return u''.join(buf) + + self._next = buffering_next + + def __iter__(self): + return self + + def next(self): + return self._next() diff --git a/jinja2/filters.py b/jinja2/filters.py index db0ea22..7e71b93 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -16,8 +16,7 @@ except ImportError: itemgetter = lambda a: lambda b: b[a] from urllib import urlencode, quote from jinja2.utils import escape, pformat -from jinja2.nodes import Undefined - +from jinja2.runtime import Undefined _striptags_re = re.compile(r'(|<[^>]*>)') diff --git a/jinja2/nodes.py b/jinja2/nodes.py index 02eb3c0..40aae3c 100644 --- a/jinja2/nodes.py +++ b/jinja2/nodes.py @@ -16,7 +16,6 @@ import operator from itertools import chain, izip from collections import deque from copy import copy -from jinja2.runtime import Undefined, subscribe _binop_to_func = { @@ -463,7 +462,7 @@ class Subscript(Expr): if self.ctx != 'load': raise Impossible() try: - return subscribe(self.node.as_const(), self.arg.as_const()) + return environmen.subscribe(self.node.as_const(), self.arg.as_const()) except: raise Impossible() diff --git a/jinja2/optimizer.py b/jinja2/optimizer.py index 48155c5..bc23fcb 100644 --- a/jinja2/optimizer.py +++ b/jinja2/optimizer.py @@ -21,7 +21,7 @@ """ from jinja2 import nodes from jinja2.visitor import NodeVisitor, NodeTransformer -from jinja2.runtime import subscribe, LoopContext +from jinja2.runtime import LoopContext def optimize(node, environment, context_hint=None): diff --git a/jinja2/runtime.py b/jinja2/runtime.py index 9a6d9f2..d3864b8 100644 --- a/jinja2/runtime.py +++ b/jinja2/runtime.py @@ -14,19 +14,8 @@ except ImportError: defaultdict = None -__all__ = ['subscribe', 'LoopContext', 'StaticLoopContext', 'TemplateContext', - 'Macro', 'IncludedTemplate', 'Undefined', 'TemplateData'] - - -def subscribe(obj, argument): - """Get an item or attribute of an object.""" - try: - return getattr(obj, str(argument)) - except (AttributeError, UnicodeError): - try: - return obj[argument] - except (TypeError, LookupError): - return Undefined(obj, argument) +__all__ = ['LoopContext', 'StaticLoopContext', 'TemplateContext', + 'Macro', 'IncludedTemplate', 'TemplateData'] class TemplateData(unicode): @@ -45,8 +34,9 @@ class TemplateContext(dict): the exported variables for example). """ - def __init__(self, globals, filename, blocks, standalone): + def __init__(self, environment, globals, filename, blocks, standalone): dict.__init__(self, globals) + self.environment = environment self.exported = set() self.filename = filename self.blocks = dict((k, [v]) for k, v in blocks.iteritems()) @@ -64,8 +54,8 @@ class TemplateContext(dict): try: func = self.blocks[block][-2] except LookupError: - return Undefined('super', extra='there is probably no parent ' - 'block with this name') + return self.environment.undefined('super', + extra='there is probably no parent block with this name') return SuperBlock(block, self, func) def __setitem__(self, key, value): @@ -88,10 +78,10 @@ class TemplateContext(dict): def __getitem__(self, name): if name in self: return self[name] - return Undefined(name) + return self.environment.undefined(name) else: def __missing__(self, key): - return Undefined(key) + return self.environment.undefined(key) def __repr__(self): return '<%s %s of %r>' % ( @@ -227,7 +217,8 @@ class StaticLoopContext(LoopContextBase): class Macro(object): """Wraps a macro.""" - def __init__(self, func, name, arguments, defaults, catch_all, caller): + def __init__(self, environment, func, name, arguments, defaults, catch_all, caller): + self._environment = environment self._func = func self.name = name self.arguments = arguments @@ -251,13 +242,15 @@ class Macro(object): try: value = self.defaults[idx - arg_count] except IndexError: - value = Undefined(name, extra='parameter not provided') + value = self._environment.undefined(name, + extra='parameter not provided') arguments['l_' + name] = value if self.caller: caller = kwargs.pop('caller', None) if caller is None: - caller = Undefined('caller', extra='The macro was called ' - 'from an expression and not a call block.') + caller = self._environment.undefined('caller', + extra='The macro was called from an expression and not ' + 'a call block.') arguments['l_caller'] = caller if self.catch_all: arguments['l_arguments'] = kwargs @@ -271,7 +264,10 @@ class Macro(object): class Undefined(object): - """The object for undefined values.""" + """The default undefined implementation. This undefined implementation + can be printed and iterated over, but every other access will raise a + `NameError`. Custom undefined classes must subclass this. + """ def __init__(self, name=None, attr=None, extra=None): if attr is None: @@ -282,18 +278,22 @@ class Undefined(object): if extra is not None: self._undefined_hint += ' (' + extra + ')' - def fail(self, *args, **kwargs): + def fail_with_error(self, *args, **kwargs): raise NameError(self._undefined_hint) - __getattr__ = __getitem__ = __add__ = __mul__ = __div__ = \ - __realdiv__ = __floordiv__ = __mod__ = __pos__ = __neg__ = \ - __call__ = fail - del fail + __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \ + __realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \ + __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \ + __getattr__ = __getitem__ = fail_with_error + del fail_with_error def __unicode__(self): - return '' + return u'' + + def __str__(self): + return self.__unicode__().encode('utf-8') def __repr__(self): - return 'Undefined' + return 'undefined' def __len__(self): return 0 @@ -301,3 +301,22 @@ class Undefined(object): def __iter__(self): if 0: yield None + + def __nonzero__(self): + return False + + +class DebugUndefined(Undefined): + """An undefined that returns the debug info when printed.""" + + def __unicode__(self): + return u'{{ %s }}' % self._undefined_hint + + +class StrictUndefined(Undefined): + """An undefined that barks on print and iteration.""" + + def fail_with_error(self, *args, **kwargs): + raise NameError(self._undefined_hint) + __iter__ = __unicode__ = __len__ = fail_with_error + del fail_with_error diff --git a/jinja2/sandbox.py b/jinja2/sandbox.py new file mode 100644 index 0000000..0c8b940 --- /dev/null +++ b/jinja2/sandbox.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +""" + jinja2.sandbox + ~~~~~~~~~~~~~~ + + Adds a sandbox layer to Jinja as it was the default behavior in the old + Jinja 1 releases. This sandbox is slightly different from Jinja 1 as the + default behavior is easier to use. + + The behavior can be changed by subclassing the environment. + + :copyright: Copyright 2008 by Armin Ronacher. + :license: BSD. +""" +from types import FunctionType, MethodType +from jinja2.runtime import Undefined +from jinja2.environment import Environment + + +#: maximum number of items a range may produce +MAX_RANGE = 100000 + + +def safe_range(*args): + """A range that can't generate ranges with a length of more than + MAX_RANGE items.""" + rng = xrange(*args) + if len(rng) > MAX_RANGE: + raise OverflowError('range too big') + return rng + + +def unsafe(f): + """Mark a function as unsafe.""" + f.unsafe_callable = True + return f + + +class SandboxedEnvironment(Environment): + """The sandboxed environment""" + sandboxed = True + + def __init__(self, *args, **kwargs): + Environment.__init__(self, *args, **kwargs) + self.globals['range'] = safe_range + + def is_safe_attribute(self, obj, attr): + """The sandboxed environment will call this method to check if the + attribute of an object is safe to access. Per default all attributes + starting with an underscore are considered private as well as the + special attributes of functions and methods. + """ + if attr.startswith('_'): + return False + if isinstance(obj, FunctionType): + return not attr.startswith('func_') + if isinstance(obj, MethodType): + return not attr.startswith('im_') + return True + + def is_safe_callable(self, obj): + """Check if an object is safely callable. Per default a function is + considered safe unless the `unsafe_callable` attribute exists and is + truish. Override this method to alter the behavior, but this won't + affect the `unsafe` decorator from this module. + """ + return not getattr(obj, 'unsafe_callable', False) + + def subscribe(self, obj, arg): + """Subscribe an object from sandboxed code.""" + try: + return obj[arg] + except (TypeError, LookupError): + if not self.is_safe_attribute(obj, arg): + return Undefined(obj, arg, extra='attribute unsafe') + try: + return getattr(obj, str(arg)) + except (AttributeError, UnicodeError): + return Undefined(obj, arg) + + def call(__self, __obj, *args, **kwargs): + """Call an object from sandboxed code.""" + # the double prefixes are to avoid double keyword argument + # errors when proxying the call. + if not __self.is_safe_callable(__obj): + raise TypeError('%r is not safely callable' % (__obj,)) + return __obj(*args, **kwargs)