changeset 1031:5dccab13ec85 trunk

Fix infinite recursion in template inlining (fixes #584).
author hodgestar
date Wed, 19 Mar 2014 13:52:01 +0000
parents 7d4414a4a5d0
children e02843c0fecc
files genshi/template/base.py genshi/template/markup.py genshi/template/tests/markup.py
diffstat 3 files changed, 66 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -452,8 +452,7 @@
     @property
     def stream(self):
         if not self._prepared:
-            self._stream = list(self._prepare(self._stream))
-            self._prepared = True
+            self._prepare_self()
         return self._stream
 
     def _parse(self, source, encoding):
@@ -470,12 +469,19 @@
         """
         raise NotImplementedError
 
-    def _prepare(self, stream):
+    def _prepare_self(self, inlined=None):
+        if not self._prepared:
+            self._stream = list(self._prepare(self._stream, inlined))
+            self._prepared = True
+
+    def _prepare(self, stream, inlined):
         """Call the `attach` method of every directive found in the template.
         
         :param stream: the event stream of the template
         """
         from genshi.template.loader import TemplateNotFound
+        if inlined is None:
+            inlined = set((self.filepath,))
 
         for kind, data, pos in stream:
             if kind is SUB:
@@ -486,7 +492,7 @@
                                                       namespaces, pos)
                     if directive:
                         directives.append(directive)
-                substream = self._prepare(substream)
+                substream = self._prepare(substream, inlined)
                 if directives:
                     yield kind, (directives, list(substream)), pos
                 else:
@@ -495,27 +501,42 @@
             else:
                 if kind is INCLUDE:
                     href, cls, fallback = data
-                    if isinstance(href, basestring) and \
-                            not getattr(self.loader, 'auto_reload', True):
+                    tmpl_inlined = False
+                    if (isinstance(href, basestring) and
+                            not getattr(self.loader, 'auto_reload', True)):
                         # If the path to the included template is static, and
                         # auto-reloading is disabled on the template loader,
-                        # the template is inlined into the stream
+                        # the template is inlined into the stream provided it
+                        # is not already in the stack of templates being
+                        # processed.
+                        tmpl = None
                         try:
                             tmpl = self.loader.load(href, relative_to=pos[0],
                                                     cls=cls or self.__class__)
-                            for event in tmpl.stream:
-                                yield event
                         except TemplateNotFound:
                             if fallback is None:
                                 raise
-                            for event in self._prepare(fallback):
+                        if tmpl is not None:
+                            if tmpl.filepath not in inlined:
+                                inlined.add(tmpl.filepath)
+                                tmpl._prepare_self(inlined)
+                                for event in tmpl.stream:
+                                    yield event
+                                inlined.discard(tmpl.filepath)
+                                tmpl_inlined = True
+                        else:
+                            for event in self._prepare(fallback, inlined):
                                 yield event
+                            tmpl_inlined = True
+                    if tmpl_inlined:
                         continue
-                    elif fallback:
+                    if fallback:
                         # Otherwise the include is performed at run time
-                        data = href, cls, list(self._prepare(fallback))
-
-                yield kind, data, pos
+                        data = href, cls, list(
+                            self._prepare(fallback, inlined))
+                    yield kind, data, pos
+                else:
+                    yield kind, data, pos
 
     def generate(self, *args, **kwargs):
         """Apply the template to the given context data.
--- a/genshi/template/markup.py
+++ b/genshi/template/markup.py
@@ -284,10 +284,10 @@
 
             yield kind, data, pos
 
-    def _prepare(self, stream):
-        return Template._prepare(self,
-            self._extract_includes(self._interpolate_attrs(stream))
-        )
+    def _prepare(self, stream, inlined=None):
+        return Template._prepare(
+            self, self._extract_includes(self._interpolate_attrs(stream)),
+            inlined=inlined)
 
     def add_directives(self, namespace, factory):
         """Register a custom `DirectiveFactory` for a given namespace.
--- a/genshi/template/tests/markup.py
+++ b/genshi/template/tests/markup.py
@@ -580,7 +580,33 @@
         finally:
             shutil.rmtree(dirname)
 
-    def test_allow_exec_false(self): 
+    def test_include_inline_recursive(self):
+        dirname = tempfile.mkdtemp(suffix='genshi_test')
+        try:
+            file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w')
+            try:
+                file1.write(
+                    '<div xmlns:xi="http://www.w3.org/2001/XInclude"'
+                    '                xmlns:py="http://genshi.edgewall.org/">'
+                    '$depth'
+                    '<py:with vars="depth = depth + 1">'
+                    '<xi:include href="tmpl1.html"'
+                    '            py:if="depth &lt; 3"/>'
+                    '</py:with>'
+                    '</div>'
+                )
+            finally:
+                file1.close()
+
+            loader = TemplateLoader([dirname], auto_reload=False)
+            tmpl = loader.load(os.path.join(dirname, 'tmpl1.html'))
+            self.assertEqual(
+                "<div>0<div>1<div>2</div></div></div>",
+                tmpl.generate(depth=0).render(encoding=None))
+        finally:
+            shutil.rmtree(dirname)
+
+    def test_allow_exec_false(self):
         xml = ("""<?python
           title = "A Genshi Template"
           ?>
Copyright (C) 2012-2017 Edgewall Software