From aad3f4e63ff9cb323e15b4dc30e7c9f78cc1d556 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Thu, 25 Feb 2010 13:52:18 +0000
Subject: [PATCH] - introduce a new parameter "safe_substitute" to perform safe
 substitution, and enable it by default. This changes behaviour, but in
 previous case it was very hard for users to understand what's wrong in their
 notification message. - add test for this new feature and for some
 substitution features that where not tested.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@33120 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/TextDocument.py         | 23 +++--
 .../tests/testNotificationMessageModule.py    | 96 ++++++++++++++++---
 2 files changed, 100 insertions(+), 19 deletions(-)

diff --git a/product/ERP5/Document/TextDocument.py b/product/ERP5/Document/TextDocument.py
index cc903291ee..22fec5290d 100644
--- a/product/ERP5/Document/TextDocument.py
+++ b/product/ERP5/Document/TextDocument.py
@@ -33,7 +33,7 @@ from zLOG import LOG, WARNING
 from Products.ERP5Type.Base import WorkflowMethod
 from Products.CMFCore.utils import getToolByName
 from Products.CMFCore.utils import _setCacheHeaders, _ViewEmulator
-from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces
+from Products.ERP5Type import Permissions, PropertySheet, Constraint
 from Products.ERP5.Document.Document import Document, ConversionError
 from Products.ERP5Type.WebDAVSupport import TextContent
 from Products.CMFDefault.utils import isHTMLSafe
@@ -153,7 +153,7 @@ class TextDocument(Document, TextContent):
       RESPONSE.setHeader('Accept-Ranges', 'bytes')
       return data
 
-    def _substituteTextContent(self, text, **kw):
+    def _substituteTextContent(self, text, safe_substitute=True, **kw):
       # If a method for string substitutions of the text content, perform it.
       # Decode everything into unicode before the substitutions, in order to
       # avoid encoding errors.
@@ -162,7 +162,9 @@ class TextDocument(Document, TextContent):
         try:
           mapping = guarded_getattr(self, method_id)(**kw)
         except AttributeError:
-          LOG('TextDocument', WARNING, 'could not get the substitution mapping method %s from %r, so the content will not be substituted.' % (method_id, self))
+          LOG('TextDocument', WARNING, 'could not get the substitution'
+              ' mapping method %s from %r, so the content will not be'
+              ' substituted.' % (method_id, self))
           return text
 
         is_str = isinstance(text, str)
@@ -177,7 +179,10 @@ class TextDocument(Document, TextContent):
             v = str(v).decode('utf-8')
           unicode_mapping[k] = v
 
-        text = Template(text).substitute(unicode_mapping)
+        if safe_substitute:
+          text = Template(text).safe_substitute(unicode_mapping)
+        else:
+          text = Template(text).substitute(unicode_mapping)
 
         # If the original was a str, convert it back to str.
         if is_str:
@@ -186,17 +191,18 @@ class TextDocument(Document, TextContent):
       return text
 
     security.declareProtected(Permissions.View, 'asSubjectText')
-    def asSubjectText(self, substitution_method_parameter_dict=None, **kw):
+    def asSubjectText(self, substitution_method_parameter_dict=None, safe_substitute=True, **kw):
       """
         Converts the subject of the document to a textual representation.
       """
       subject = TextDocument.inheritedAttribute('asSubjectText')(self, **kw)
       if substitution_method_parameter_dict is None:
         substitution_method_parameter_dict = {}
-      return self._substituteTextContent(subject, **substitution_method_parameter_dict)
+      return self._substituteTextContent(subject, safe_substitute=safe_substitute,
+                                         **substitution_method_parameter_dict)
 
     security.declareProtected(Permissions.AccessContentsInformation, 'convert')
-    def convert(self, format, substitution_method_parameter_dict=None, **kw):
+    def convert(self, format, substitution_method_parameter_dict=None, safe_substitute=True, **kw):
       """
         Convert text using portal_transforms or oood
       """
@@ -228,7 +234,8 @@ class TextDocument(Document, TextContent):
           mime_type, result = self.getConversion(format=format)
         if substitution_method_parameter_dict is None:
           substitution_method_parameter_dict = {}
-        result = self._substituteTextContent(result, **substitution_method_parameter_dict)
+        result = self._substituteTextContent(result, safe_substitute=safe_substitute,
+                                             **substitution_method_parameter_dict)
         return mime_type, result
       else:
         # text_content is not set, return empty string instead of None
diff --git a/product/ERP5/tests/testNotificationMessageModule.py b/product/ERP5/tests/testNotificationMessageModule.py
index 0e925f6875..4322691b05 100644
--- a/product/ERP5/tests/testNotificationMessageModule.py
+++ b/product/ERP5/tests/testNotificationMessageModule.py
@@ -29,8 +29,8 @@
 import unittest
 
 import transaction
-from Testing import ZopeTestCase
 from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
+from Products.ERP5Type.tests.utils import createZODBPythonScript
 from AccessControl.SecurityManagement import newSecurityManager
 from AccessControl.SecurityManagement import getSecurityManager
 from zLOG import LOG
@@ -38,11 +38,8 @@ from Products.ERP5Type.tests.utils import DummyMailHost
 
 class TestNotificationMessageModule(ERP5TypeTestCase):
   """
-  Test notification tool
+  Test notification message module
   """
-  run_all_test = 1
-  quiet = 1
-
   def getBusinessTemplateList(self):
     return ('erp5_base',)
 
@@ -78,12 +75,7 @@ class TestNotificationMessageModule(ERP5TypeTestCase):
     transaction.commit()
     self.tic()
 
-  def test_01_get_document(self, quiet=quiet, run=run_all_test):
-    if not run: return
-    if not quiet:
-      message = 'Test type base Method'
-      ZopeTestCase._print('\n%s ' % message)
-      LOG('Testing... ', 0, message)
+  def test_01_get_document(self):
     module = self.getNotificationMessageModule()
     tool = self.getPortal().portal_notifications
     #Test Document A in english
@@ -117,6 +109,88 @@ class TestNotificationMessageModule(ERP5TypeTestCase):
     result = tool.getDocumentValue(reference='A', language='fr')
     self.assertEqual(result.getRelativeUrl(), n_m_fr_02.getRelativeUrl())
 
+
+  def test_substitution_content(self):
+    """Tests that content and subject have string.Template based substitutions
+    """
+    module = self.getNotificationMessageModule()
+    createZODBPythonScript(self.portal,
+                           'NotificationMessage_getDummySubstitionMapping',
+                           '**kw',
+                           '''return dict(a="b")''')
+    doc = module.newContent(portal_type='Notification Message',
+                            title='Test ${a}',
+                            text_content='substitution text: ${a}',
+                            text_content_substitution_mapping_method_id=
+                            'NotificationMessage_getDummySubstitionMapping')
+
+    mime, text = doc.convert('txt')
+    self.assertEqual('text/plain', mime)
+    self.assertEqual('substitution text: b', text)
+
+    self.assertEqual('Test b', doc.asSubjectText())
+
+
+  def test_substitution_content_parameters(self):
+    """Tests that we can pass parameters to convert to the substitution method,
+    by using substitution_method_parameter_dict """
+    module = self.getNotificationMessageModule()
+    createZODBPythonScript(self.portal,
+                           'NotificationMessage_getDummySubstitionMapping',
+                           '**kw',
+                           '''return kw''')
+    doc = module.newContent(portal_type='Notification Message',
+                            title='Test ${a}',
+                            text_content='substitution text: ${a}',
+                            text_content_substitution_mapping_method_id=
+                            'NotificationMessage_getDummySubstitionMapping')
+
+    mime, text = doc.convert('txt',
+                             substitution_method_parameter_dict=dict(a='b'))
+    self.assertEqual('substitution text: b', text)
+
+
+  def test_substitution_content_and_convert(self):
+    """Tests that substitution also works with different target format.
+    """
+    module = self.getNotificationMessageModule()
+    createZODBPythonScript(self.portal,
+                           'NotificationMessage_getDummySubstitionMapping',
+                           '**kw',
+                           '''return dict(a="b")''')
+    doc = module.newContent(portal_type='Notification Message',
+                            text_format='text/html',
+                            text_content='substitution text: <em>${a}</em>',
+                            text_content_substitution_mapping_method_id=
+                            'NotificationMessage_getDummySubstitionMapping')
+
+    mime, text = doc.convert('txt')
+    self.assertEqual('substitution text: b', text)
+
+  def test_safe_substitution_content(self):
+    """Tests that 'safe' substitution is performed, unless safe_substitute is
+    explicitly passed to False.
+    """
+    module = self.getNotificationMessageModule()
+    createZODBPythonScript(self.portal,
+                           'NotificationMessage_getDummySubstitionMapping',
+                           '**kw',
+                           '''return dict(a="b")''')
+    doc = module.newContent(portal_type='Notification Message',
+                            title='${b}',
+                            text_content='substitution text: ${b}',
+                            text_content_substitution_mapping_method_id=
+                            'NotificationMessage_getDummySubstitionMapping')
+
+    mime, text = doc.convert('txt')
+    self.assertEqual('substitution text: ${b}', text)
+    self.assertEqual('${b}', doc.asSubjectText())
+
+    self.assertRaises(KeyError, doc.convert, 'txt', safe_substitute=False)
+    self.assertRaises(KeyError, doc.convert, 'html', safe_substitute=False)
+    self.assertRaises(KeyError, doc.asSubjectText, safe_substitute=False)
+
+
 def test_suite():
   suite = unittest.TestSuite()
   suite.addTest(unittest.makeSuite(TestNotificationMessageModule))
-- 
2.30.9