added sandbox and exchageable undefined objects
authorArmin Ronacher <armin.ronacher@active-4.com>
Mon, 14 Apr 2008 20:53:58 +0000 (22:53 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Mon, 14 Apr 2008 20:53:58 +0000 (22:53 +0200)
--HG--
branch : trunk

jinja2/__init__.py
jinja2/compiler.py
jinja2/defaults.py
jinja2/environment.py
jinja2/filters.py
jinja2/nodes.py
jinja2/optimizer.py
jinja2/runtime.py
jinja2/sandbox.py [new file with mode: 0644]

index b54c504553bccd3ee7c8879d9ca25b29bf0651c6..dcebefcae2e45512415f31ffae261b161900d31a 100644 (file)
@@ -57,3 +57,4 @@
     :license: BSD, see LICENSE for more details.
 """
 from jinja2.environment import Environment
+from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined
index 4ddacd9084e994183ef77b4edb1338e066cda30a..e299c6ff29b6bc155023f962695ba76ac2410d3a 100644 (file)
@@ -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(')')
 
index 4ad0e3a23f9a4264210467d40bd348d283cabdbb..79591802c872329c8eaa6a2f09876ddfceba822d 100644 (file)
@@ -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']
index 9d2371d5c8b47ced0a1301a39fa3f89ae87b53eb..33e8f76f034d6fb3a6f8792dce8a3246c0b2f436 100644 (file)
@@ -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()
index db0ea228674907f64ecda83c088f26cbcc16b864..7e71b93af24ae47aa71e8683cb4751324a86668e 100644 (file)
@@ -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'(<!--.*?-->|<[^>]*>)')
index 02eb3c0e618ae6dabb04e0bbd4086499f9f184a2..40aae3cdd45473d5ffe80c0eaeb67f3791d47fb2 100644 (file)
@@ -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()
 
index 48155c5dddb261054b1ce8a0ad757cc6d90813b9..bc23fcbd9af25e1dbc33f14f53ce4e8770360c3f 100644 (file)
@@ -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):
index 9a6d9f2864fd2fe76f561ebb5dde0489d84d8a89..d3864b863c5d48daf8a0ac681c8b5065b4cddbce 100644 (file)
@@ -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 (file)
index 0000000..0c8b940
--- /dev/null
@@ -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)