documentation update
authorArmin Ronacher <armin.ronacher@active-4.com>
Wed, 7 May 2008 10:17:18 +0000 (12:17 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Wed, 7 May 2008 10:17:18 +0000 (12:17 +0200)
--HG--
branch : trunk

docs/_static/style.css
docs/api.rst
docs/index.rst
docs/sandbox.rst [new file with mode: 0644]
docs/switching.rst [new file with mode: 0644]
jinja2/environment.py
jinja2/exceptions.py
jinja2/runtime.py
jinja2/sandbox.py
tests/test_security.py

index 3cce344962c2885accae6e8a1700c611ac13cbd0..46748d052355907dfd6628e2a19d83bd3c2715c1 100644 (file)
@@ -196,6 +196,10 @@ tt {
     border-bottom: 1px solid #eee;
 }
 
+a.reference:hover tt {
+    border-bottom-color: #aaa;
+}
+
 cite {
     /* abusing <cite>, it's generated by ReST for `x` */
     font-size: 13px;
@@ -291,7 +295,8 @@ table.indextable dl dd a {
 }
 
 dl.function dt,
-dl.class dt {
+dl.class dt,
+dl.exception dt {
     font-weight: normal;
 }
 
index 24e27103c75eaaa72d61d2ece51104b9d6fa054d..82bb531076aa6015e921e70887644c73729e854b 100644 (file)
@@ -48,7 +48,7 @@ High Level API
 --------------
 
 .. autoclass:: jinja2.environment.Environment([options])
-    :members: from_string, get_template, join_path, parse
+    :members: from_string, get_template, join_path, parse, lex
 
     .. attribute:: shared
 
@@ -174,6 +174,8 @@ The Context
         block (latest in the inheritance chain).
 
 
+.. _loaders:
+
 Loaders
 -------
 
@@ -232,15 +234,15 @@ functions to a Jinja2 environment.
 Exceptions
 ----------
 
-.. autoclass:: jinja2.exceptions.TemplateError
+.. autoexception:: jinja2.exceptions.TemplateError
 
-.. autoclass:: jinja2.exceptions.UndefinedError
+.. autoexception:: jinja2.exceptions.UndefinedError
 
-.. autoclass:: jinja2.exceptions.TemplateNotFound
+.. autoexception:: jinja2.exceptions.TemplateNotFound
 
-.. autoclass:: jinja2.exceptions.TemplateSyntaxError
+.. autoexception:: jinja2.exceptions.TemplateSyntaxError
 
-.. autoclass:: jinja2.exceptions.TemplateAssertionError
+.. autoexception:: jinja2.exceptions.TemplateAssertionError
 
 
 .. _writing-filters:
index eb442cbfd18b877d41315cc69ed70ad3a677fc8d..308563a4ef348d436a002ce1bc06af17d785efda 100644 (file)
@@ -10,9 +10,11 @@ fast and secure.
 
    intro
    api
+   sandbox
    templates
    extensions
    integration
+   switching
 
    changelog
 
diff --git a/docs/sandbox.rst b/docs/sandbox.rst
new file mode 100644 (file)
index 0000000..a919df8
--- /dev/null
@@ -0,0 +1,27 @@
+Sandbox
+=======
+
+The Jinja2 sandbox can be used to evaluate untrusted code.  Access to unsafe
+attributes and methods is prohibited.
+
+Assuming `env` is a :class:`SandboxedEnvironment` in the default configuration
+the following piece of code shows how it works:
+
+>>> env.from_string("{{ func.func_code }}").render(func=lambda:None)
+u''
+>>> env.from_string("{{ func.func_code.do_something }}").render(func=lambda:None)
+Traceback (most recent call last):
+  ...
+SecurityError: access to attribute 'func_code' of 'function' object is unsafe.
+
+
+.. module:: jinja2.sandbox
+
+.. autoclass:: SandboxedEnvironment([options])
+    :members: is_safe_attribute, is_safe_callable
+
+.. autoexception:: SecurityError
+
+.. autofunction:: unsafe
+
+.. autofunction:: is_internal_attribute
diff --git a/docs/switching.rst b/docs/switching.rst
new file mode 100644 (file)
index 0000000..0ca2990
--- /dev/null
@@ -0,0 +1,217 @@
+Switching from other Template Engines
+=====================================
+
+.. highlight:: html+jinja
+
+If you have used a different template engine in the past and want to swtich
+to Jinja2 here is a small guide that shows the basic syntatic and semantic
+changes between some common, similar text template engines for Python.
+
+Jinja1
+------
+
+Jinja2 is mostly compatible with Jinja1 in terms of API usage and template
+syntax.  The differences between Jinja1 and 2 are explained in the following
+list.
+
+API
+~~~
+
+Loaders
+    Jinja2 uses a different loader API.  Because the internal representation
+    of templates changed there is no longer support for external caching
+    systems such as memcached.  The memory consumed by templates is comparable
+    with regular Python modules now and external caching doesn't give any
+    advantage.  If you have used a custom loader in the past have a look at
+    the new :ref:`loader API <loaders>`.
+
+Loading templates from strings
+    In the past it was possible to generate templates from a string with the
+    default environment configuration by using `jinja.from_string`.  Jinja2
+    provides a :class:`Template` class that can be used to do the same, but
+    with optional additional configuration.
+
+Automatic unicode conversion
+    Jinja1 performed automatic conversion of bytestrings in a given encoding
+    into unicode objects.  This conversion is no longer implemented as it
+    was inconsistent as most libraries are using the regular Python ASCII
+    bytestring to Unicode conversion.  An application powered by Jinja2
+    *has to* use unicode internally everywhere or make sure that Jinja2 only
+    gets unicode strings passed.
+
+i18n
+    Jinja1 used custom translators for internationalization.  i18n is now
+    available as Jinja2 extension and uses a simpler, more gettext friendly
+    interface and has support for babel.  For more details see
+    :ref:`i18n-extension`.
+
+Internal methods
+    Jinja1 exposed a few internal methods on the environment object such
+    as `call_function`, `get_attribute` and others.  While they were marked
+    as being an internal method it was possible to override them.  Jinja2
+    doesn't have equivalent methods.
+
+Sandbox
+    Jinja1 was running sandbox mode by default.  Few applications actually
+    used that feature so it became optional in Jinja2.  For more details
+    about the sandboxed execution see :class:`SandboxedEnvironment`.
+
+Context
+    Jinja1 had a stacked context as storage for variables passed to the
+    environment.  In Jinja2 a similar object exists but it doesn't allow
+    modifications nor is it a singleton.  As inheritance is dynamic now
+    multiple context objects may exist during template evaluation.
+
+
+Templates
+~~~~~~~~~
+
+Jinja2 has mostly the same syntax as Jinja1.  The only difference is that
+assigning variables doesn't use `set` as keyword now.  The following
+example shows a Jinja1 variable assignment::
+
+    {% set foo = 42 %}
+
+In Jinja2 the `set` is ommited::
+
+    {% foo = 42 %}
+
+Additionally macros require parentheses around the argument list now.
+
+Jinja2 allows dynamic inheritance now and dynamic includes.  The old helper
+function `rendertemplate` is gone now, `include` can be used instead.
+Additionally includes no longer import macros and variable assignments, for
+that the new `import` tag is used.  This concept is explained in the
+:ref:`import` documentation.
+
+Currently there is no support for the `recursive` modifier of for loops!
+
+
+Django
+------
+
+If you have previously worked with Django templates, you should find
+Jinja2 very familiar.  In fact, most of the syntax elements look and
+work the same.
+
+However, Jinja2 provides some more syntax elements covered in the
+documentation and some work a bit different.
+
+This section covers the template changes.  As the API is fundamentally
+different we won't cover it here.
+
+Method Calls
+~~~~~~~~~~~~
+
+In Django method calls work implicitly.  With Jinja2 you have to specify that
+you want to call an object.  Thus this Django code::
+
+    {% for page in user.get_created_pages %}
+        ...
+    {% endfor %}
+    
+will look like this in Jinja::
+
+    {% for page in user.get_created_pages() %}
+        ...
+    {% endfor %}
+
+This allows you to pass variables to the function which is also used for macros
+which is not possible in Django.
+
+Conditions
+~~~~~~~~~~
+
+In Django you can use the following constructs to check for equality::
+
+    {% ifequals foo "bar" %}
+        ...
+    {% else %}
+        ...
+    {% endifequals %}
+
+In Jinja2 you can use the normal if statement in combination with operators::
+
+    {% if foo == 'bar' %}
+        ...
+    {% else %}
+        ...
+    {% endif %}
+
+You can also have multiple elif branches in your template::
+
+    {% if something %}
+        ...
+    {% elif otherthing %}
+        ...
+    {% elif foothing %}
+        ...
+    {% else %}
+        ...
+    {% endif %}
+
+Filter Arguments
+~~~~~~~~~~~~~~~~
+
+Jinja2 provides more than one argument for filters.  Also the syntax for
+argument passing is different.  A template that looks like this in Django::
+
+    {{ items|join:", " }}
+
+looks like this in Jinja2::
+
+    {{ items|join(', ') }}
+
+In fact it's a bit more verbose but it allows different types of arguments -
+including variables - and more than one of them.
+
+Tests
+~~~~~
+
+In addition to filters there also are tests you can perform using the is
+operator.  Here are some examples::
+
+    {% if user.user_id is odd %}
+        {{ user.username|e }} is odd
+    {% else %}
+        hmm. {{ user.username|e }} looks pretty normal
+    {% endif %}
+
+
+Mako
+----
+
+.. highlight:: html+mako
+
+If you have used Mako so far and want to switch to Jinja2 you can configure
+Jinja2 to look more like Mako:
+
+.. sourcecode:: python
+
+    env = Environment('<%', '%>', '${', '}', '%')
+
+Once the environment is configure like that Jinja2 should be able to interpret
+a small subset of Mako templates.  Jinja2 does not support embedded Python code
+so you would have to move that out of the template.  The syntax for defs (in
+Jinja2 defs are called macros) and template inheritance is different too.  The
+following Mako template::
+
+    <%inherit file="layout.html" />
+    <%def name="title()">Page Title</%def>
+    <ul>
+    % for item in list:
+        <li>${item}</li>
+    % endfor
+    </ul>
+
+Looks like this in Jinja2 with the above configuration::
+
+    <% extends "layout.html" %>
+    <% block title %>Page Title<% endblock %>
+    <% block body %>
+    <ul>
+    % for item in list:
+        <li>${item}</li>
+    % endfor
+    </ul>
+    <% endblock %>
index 23e77b6dac2323d455cf267591110e80f4aa0254..4134103e17f681ea4d1d7b660423238e710dd9fe 100644 (file)
@@ -293,11 +293,13 @@ class Environment(object):
             exc_type, exc_value, tb = translate_syntax_error(e)
             raise exc_type, exc_value, tb
 
-    def lex(self, source, name=None):
+    def lex(self, source, filename=None):
         """Lex the given sourcecode and return a generator that yields
         tokens as tuples in the form ``(lineno, token_type, value)``.
+        This can be useful for :ref:`extension development <writing-extensions>`
+        and debugging templates.
         """
-        return self.lexer.tokeniter(source, name)
+        return self.lexer.tokeniter(source, filename)
 
     def compile(self, source, name=None, filename=None, globals=None,
                 raw=False):
index e5b156b9f90fafa646ac54c1398c7f9b92cdfce4..265395266e567fdc297715ef7de1a4cf9a8139bd 100644 (file)
@@ -18,6 +18,12 @@ class UndefinedError(TemplateError):
     """Raised if a template tries to operate on :class:`Undefined`."""
 
 
+class SecurityError(TemplateError):
+    """Raised if a template tries to do something insecure if the
+    sandbox is enabled.
+    """
+
+
 class TemplateNotFound(IOError, LookupError, TemplateError):
     """Raised if a template does not exist."""
 
index 829de41df2a68bf05acd980f43070b57c047d0a7..8fb3f0b74ebd485a7dc10c09d0265fb78c048552 100644 (file)
@@ -306,7 +306,7 @@ def fail_with_undefined_error(self, *args, **kwargs):
             )
     else:
         hint = self._undefined_hint
-    raise UndefinedError(hint)
+    raise self._undefined_exception(hint)
 
 
 class Undefined(object):
@@ -323,12 +323,14 @@ class Undefined(object):
       ...
     jinja2.exceptions.UndefinedError: 'foo' is undefined
     """
-    __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name')
+    __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name',
+                 '_undefined_exception')
 
-    def __init__(self, hint=None, obj=None, name=None):
+    def __init__(self, hint=None, obj=None, name=None, exc=UndefinedError):
         self._undefined_hint = hint
         self._undefined_obj = obj
         self._undefined_name = name
+        self._undefined_exception = exc
 
     __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \
     __realdiv__ = __rrealdiv__ = __floordiv__ = __rfloordiv__ = \
index e0273019ee684fedb0838db92128e9132ab90a90..4b9ac13832a90035e3d033161c5501b054b0a488 100644 (file)
@@ -16,6 +16,7 @@ from types import FunctionType, MethodType, TracebackType, CodeType, \
      FrameType, GeneratorType
 from jinja2.runtime import Undefined
 from jinja2.environment import Environment
+from jinja2.exceptions import SecurityError
 
 
 #: maximum number of items a range may produce
@@ -41,13 +42,55 @@ def safe_range(*args):
 
 
 def unsafe(f):
-    """Mark a function as unsafe."""
+    """
+    Mark a function or method as unsafe::
+
+        @unsafe
+        def delete(self):
+            pass
+    """
     f.unsafe_callable = True
     return f
 
 
+def is_internal_attribute(obj, attr):
+    """Test if the attribute given is an internal python attribute.  For
+    example this function returns `True` for the `func_code` attribute of
+    python objects.  This is useful if the environment method
+    :meth:`~SandboxedEnvironment.is_safe_attribute` is overriden.
+
+    >>> from jinja2.sandbox import is_internal_attribute
+    >>> is_internal_attribute(lambda: None, "func_code")
+    True
+    >>> is_internal_attribute((lambda x:x).func_code, 'co_code')
+    True
+    >>> is_internal_attribute(str, "upper")
+    False
+    """
+    if isinstance(obj, FunctionType):
+        return attr in UNSAFE_FUNCTION_ATTRIBUTES
+    if isinstance(obj, MethodType):
+        return attr in UNSAFE_FUNCTION_ATTRIBUTES or \
+               attr in UNSAFE_METHOD_ATTRIBUTES
+    if isinstance(obj, type):
+        return attr == 'mro'
+    if isinstance(obj, (CodeType, TracebackType, FrameType)):
+        return True
+    if isinstance(obj, GeneratorType):
+        return attr == 'gi_frame'
+    return attr.startswith('__')
+
+
 class SandboxedEnvironment(Environment):
-    """The sandboxed environment"""
+    """The sandboxed environment.  It works like the regular environment but
+    tells the compiler to generate sandboxed code.  Additionally subclasses of
+    this environment may override the methods that tell the runtime what
+    attributes or functions are safe to access.
+
+    If the template tries to access insecure code a :exc:`SecurityError` is
+    raised.  However also other exceptions may occour during the rendering so
+    the caller has to ensure that all exceptions are catched.
+    """
     sandboxed = True
 
     def __init__(self, *args, **kwargs):
@@ -58,22 +101,10 @@ class SandboxedEnvironment(Environment):
         """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.
+        special attributes of internal python objects as returned by the
+        :func:`is_internal_attribute` function.
         """
-        if attr.startswith('_'):
-            return False
-        if isinstance(obj, FunctionType):
-            return attr not in UNSAFE_FUNCTION_ATTRIBUTES
-        if isinstance(obj, MethodType):
-            return attr not in UNSAFE_FUNCTION_ATTRIBUTES and \
-                   attr not in UNSAFE_METHOD_ATTRIBUTES
-        if isinstance(obj, type):
-            return attr != 'mro'
-        if isinstance(obj, (CodeType, TracebackType, FrameType)):
-            return False
-        if isinstance(obj, GeneratorType):
-            return attr != 'gi_frame'
-        return True
+        return not (attr.startswith('_') or is_internal_attribute(obj, attr))
 
     def is_safe_callable(self, obj):
         """Check if an object is safely callable.  Per default a function is
@@ -103,7 +134,7 @@ class SandboxedEnvironment(Environment):
                                       ' unsafe.' % (
                     argument,
                     obj.__class__.__name__
-                ), name=argument)
+                ), name=argument, exc=SecurityError)
         return self.undefined(obj=obj, name=argument)
 
     def call(__self, __obj, *args, **kwargs):
@@ -111,5 +142,5 @@ class SandboxedEnvironment(Environment):
         # 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,))
+            raise SecurityError('%r is not safely callable' % (__obj,))
         return __obj(*args, **kwargs)
index 331e8b97dc58efb8b72b0d2bb97ea73a3c9cf1bf..803c5e7f4687c1481fa6bbffba8e0928ee5fc1b6 100644 (file)
@@ -35,14 +35,14 @@ test_unsafe = '''
 >>> env.from_string("{{ foo.foo() }}").render(foo=MODULE.PrivateStuff())
 Traceback (most recent call last):
     ...
-TypeError: <bound method PrivateStuff.foo of PrivateStuff> is not safely callable
+SecurityError: <bound method PrivateStuff.foo of PrivateStuff> is not safely callable
 >>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PrivateStuff())
 u'23'
 
 >>> env.from_string("{{ foo._foo() }}").render(foo=MODULE.PublicStuff())
 Traceback (most recent call last):
     ...
-UndefinedError: access to attribute '_foo' of 'PublicStuff' object is unsafe.
+SecurityError: access to attribute '_foo' of 'PublicStuff' object is unsafe.
 >>> env.from_string("{{ foo.bar() }}").render(foo=MODULE.PublicStuff())
 u'23'
 
@@ -53,7 +53,7 @@ u''
 >>> env.from_string("{{ foo.__class__.__subclasses__() }}").render(foo=42)
 Traceback (most recent call last):
     ...
-UndefinedError: access to attribute '__class__' of 'int' object is unsafe.
+SecurityError: access to attribute '__class__' of 'int' object is unsafe.
 '''