Permalink
Browse files

added sandbox and exchageable undefined objects

--HG--
branch : trunk
  • Loading branch information...
1 parent f437fa3 commit c63243e036012c4ef6a9c04329e2e45e0a21bd31 @mitsuhiko mitsuhiko committed Apr 14, 2008
Showing with 230 additions and 49 deletions.
  1. +1 −0 jinja2/__init__.py
  2. +11 −8 jinja2/compiler.py
  3. +4 −6 jinja2/defaults.py
  4. +75 −0 jinja2/environment.py
  5. +1 −2 jinja2/filters.py
  6. +1 −2 jinja2/nodes.py
  7. +1 −1 jinja2/optimizer.py
  8. +49 −30 jinja2/runtime.py
  9. +87 −0 jinja2/sandbox.py
View
1 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
View
19 jinja2/compiler.py
@@ -457,8 +457,8 @@ def visit_Template(self, node, frame=None):
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 @@ def visit_For(self, node, frame):
# 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 @@ def visit_Macro(self, node, frame):
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 @@ def visit_CallBlock(self, node, frame):
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 @@ def visit_Subscript(self, node, frame):
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 @@ def visit_CondExpr(self, node, frame):
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(')')
View
10 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']
View
75 jinja2/environment.py
@@ -12,6 +12,7 @@
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 @@ def __init__(self,
line_statement_prefix=None,
trim_blocks=False,
optimized=True,
+ undefined=Undefined,
loader=None):
"""Here the possible initialization parameters:
@@ -55,9 +62,13 @@ def __init__(self,
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 @@ def __init__(self,
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 @@ def __init__(self,
# 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 @@ def generate(self, *args, **kwargs):
# 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()
View
3 jinja2/filters.py
@@ -16,8 +16,7 @@
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'(<!--.*?-->|<[^>]*>)')
View
3 jinja2/nodes.py
@@ -16,7 +16,6 @@
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 @@ def as_const(self):
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()
View
2 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):
View
79 jinja2/runtime.py
@@ -14,19 +14,8 @@
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 @@ def super(self, block):
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 @@ def get_exported(self):
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 @@ def make_static(self):
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 @@ def __call__(self, *args, **kwargs):
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 @@ def __repr__(self):
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,22 +278,45 @@ def __init__(self, name=None, attr=None, extra=None):
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
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
View
87 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)

0 comments on commit c63243e

Please sign in to comment.