SVN: CMF/trunk/ - CMFCore.CachingPolicyManager: Prevent firing of caching policies

Jens Vagelpohl <[email protected]>
Newsgroups gmane.comp.web.zope.cmf.cvs
Message-ID <[email protected]>
Log message for revision 71111:
  - CMFCore.CachingPolicyManager: Prevent firing of caching policies
    for templates (DTML or ZPT) that are rendered in-line (without a
    separate request) while rendering the requested content item's view.
    (http://www.zope.org/Collectors/CMF/456)
  

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFActionIcons/DEPENDENCIES.txt
  U   CMF/trunk/CMFCalendar/DEPENDENCIES.txt
  U   CMF/trunk/CMFCore/DEPENDENCIES.txt
  U   CMF/trunk/CMFCore/tests/base/dummy.py
  U   CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
  U   CMF/trunk/CMFCore/utils.py
  U   CMF/trunk/CMFDefault/DEPENDENCIES.txt
  U   CMF/trunk/CMFTopic/DEPENDENCIES.txt
  U   CMF/trunk/CMFUid/DEPENDENCIES.txt
  U   CMF/trunk/DCWorkflow/DEPENDENCIES.txt

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CHANGES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -27,10 +27,15 @@
 
   Bug Fixes
 
+    - CMFCore.CachingPolicyManager: Prevent firing of caching policies
+      for templates (DTML or ZPT) that are rendered in-line (without a 
+      separate request) while rendering the requested content item's view.
+      (http://www.zope.org/Collectors/CMF/456)
+
     - CMFDefault RegistrationTool: Fixed too restrictive email checking.
       The new 'checkEmailAddress' function is now used.
 
-    - Fixed test breakage induced by use of Z3 pagetempalates in Zope 2.10+.
+    - Fixed test breakage induced by use of Z3 pagetemplates in Zope 2.10+.
 
     - CMFDefault skins: Fixed encoding issues in welcome and reminder emails.
       'password_email' and 'registered_email' now encode their return value
@@ -38,7 +43,7 @@
 
   Others
 
-    - The CMF now depends on Zope 2.10.0.
+    - The CMF now depends on Zope 2.10.1
 
 
 CMF 2.1.0-alpha (2006/10/09)

Modified: CMF/trunk/CMFActionIcons/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFActionIcons/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFActionIcons/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup

Modified: CMF/trunk/CMFCalendar/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFCalendar/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCalendar/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,4 +1,4 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 CMFDefault
 GenericSetup

Modified: CMF/trunk/CMFCore/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFCore/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,2 +1,2 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 GenericSetup

Modified: CMF/trunk/CMFCore/tests/base/dummy.py
===================================================================
--- CMF/trunk/CMFCore/tests/base/dummy.py	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/tests/base/dummy.py	2006-11-12 17:06:03 UTC (rev 71111)
@@ -118,6 +118,7 @@
         self.reset()
         self.catalog = kw.get('catalog',0)
         self.url = kw.get('url',None)
+        self.view_id = kw.get('view_id',None)
 
     def manage_afterAdd(self, item, container):
         self.after_add_called = 1
@@ -162,6 +163,17 @@
     def Type( self ):
         return 'Dummy Content Title'
 
+    def __call__(self):
+        if self.view_id is None:
+           return DummyContent.inheritedAttribute('__call__')(self)
+        else:
+           # view_id control for testing
+           template = getattr(self, self.view_id)
+           if getattr(aq_base(template), 'isDocTemp', 0):
+               return template(self, self.REQUEST, self.REQUEST['RESPONSE'])
+           else:
+               return template()
+
 DummyFactory = Factory(DummyContent)
 
 

Modified: CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2006-11-12 17:06:03 UTC (rev 71111)
@@ -20,12 +20,14 @@
 
 import base64
 import os
+from os.path import join as path_join
 
 from AccessControl.SecurityManagement import newSecurityManager
 from App.Common import rfc1123_date
 from DateTime.DateTime import DateTime
 
 from Products.CMFCore.FSPageTemplate import FSPageTemplate
+from Products.CMFCore.FSDTMLMethod import FSDTMLMethod
 from Products.CMFCore.testing import FunctionalZCMLLayer
 from Products.CMFCore.testing import TraversingZCMLLayer
 from Products.CMFCore.tests.base.dummy import DummyContent
@@ -842,12 +844,341 @@
         self.assertEqual(response.getStatus(), 200)
         self._cleanup()
 
+class FSObjMaker(FSDVTest):
 
+    def _makeFSPageTemplate( self, id, filename ):
+        path = path_join(self.skin_path_name, filename)
+        return FSPageTemplate( id, path )
+
+    def _makeFSDTMLMethod( self, id, filename ):
+        path = path_join(self.skin_path_name, filename)
+        return FSDTMLMethod( id, path )
+
+class NestedTemplateTests( RequestTest, FSObjMaker ):
+
+    layer = TraversingZCMLLayer
+
+    def setUp(self):
+        FSObjMaker.setUp(self)
+        RequestTest.setUp(self)
+
+        # Create a fake portal and the tools we need
+        self.portal = DummySite(id='portal').__of__(self.root)
+        self.portal._setObject('portal_types', DummyTool())
+
+        from Products.CMFCore import CachingPolicyManager
+        CachingPolicyManager.manage_addCachingPolicyManager(self.portal)
+
+    def tearDown(self):
+        RequestTest.tearDown(self)
+        FSObjMaker.tearDown(self)
+
+    def test_subtemplate_cpm_1( self ):
+        # test that subtemplates dont call the cpm
+        # set up site
+        portal = self.portal
+        now = DateTime()
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_op2',
+                      predicate = 'python:view=="output_page_2"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        content = DummyContent(id='content', view_id='output_page_1')
+        content.modified_date = now
+        portal._setObject('content', content)
+
+        output_page_1 = self._makeFSPageTemplate('output_page_1', 'output_page_1.zpt')
+        output_page_2 = self._makeFSPageTemplate('output_page_2', 'output_page_2.zpt')
+        portal._setObject('output_page_1', output_page_1)
+        portal._setObject('output_page_2', output_page_2)
+
+        portal.content()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_subtemplate_cpm_2( self ):
+        # test that calling content from a template doesnt call the cpm
+        # just calling an FSDTMLMethod directly from another template does
+        # not activate the bug because RESPONSE is not passed in
+        portal = self.portal
+        now = DateTime()
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_op4',
+                      predicate = 'python:view=="output_page_4"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        content = DummyContent(id='content', view_id='output_page_3')
+        content.modified_date = now
+        portal._setObject('content', content)
+        content2 = DummyContent(id='content2', view_id='output_page_4')
+        content2.modified_date = now
+        portal._setObject('content2', content2)
+
+        output_page_3 = self._makeFSDTMLMethod('output_page_3', 'output_page_3.dtml')
+        output_page_4 = self._makeFSDTMLMethod('output_page_4', 'output_page_4.dtml')
+        portal._setObject('output_page_4',output_page_4)
+        portal._setObject('output_page_3',output_page_3)
+
+        # call the content
+        portal.content()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_subtemplate_cpm_3( self ):
+        # test a bigger mix of zpt templates
+        # set up site
+        portal = self.portal
+        now = DateTime()
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_nv1',
+                      predicate = 'python:view=="nested_view_1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        doc1 = DummyContent(id='doc1', view_id='nested_view')
+        doc1.modified_date = now
+        portal._setObject('doc1',doc1)
+        doc2 = DummyContent(id='doc2', view_id='nested_view_1')
+        doc2.modified_date = now
+        portal._setObject('doc2',doc2)
+        doc3 = DummyContent(id='doc3', view_id='nested_view_2')
+        doc3.modified_date = now
+        portal._setObject('doc3',doc3)
+
+        nested_view = self._makeFSPageTemplate('nested_view', 'nested_view.zpt')
+        nested_view_1 = self._makeFSPageTemplate('nested_view_1', 'nested_view_1.zpt')
+        nested_view_2 = self._makeFSPageTemplate('nested_view_2', 'nested_view_2.zpt')
+        portal._setObject('nested_view', nested_view)
+        portal._setObject('nested_view_1', nested_view_1)
+        portal._setObject('nested_view_2', nested_view_2)
+
+        data = portal.doc1()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_mixed_subtemplate_cpm( self ):
+        # test a mix of zpt and dtml templates
+        # set up site
+        now = DateTime()
+        portal = self.portal
+        cpm = portal.caching_policy_manager
+        cpm.addPolicy(policy_id = 'policy_nv1',
+                      predicate = 'python:view=="nested_view_1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        doc1 = DummyContent(id='doc1', view_id='nested_view', modified_date=now)
+        portal._setObject('doc1', doc1)
+        doc2 = DummyContent(id='doc2', view_id='nested_view_1', modified_date=now)
+        portal._setObject('doc2', doc2)
+        doc3 = DummyContent(id='doc3', view_id='nested_view_2', modified_date=now)
+        portal._setObject('doc3', doc3)
+
+        nested_view = self._makeFSPageTemplate('nested_view', 'nested_view.zpt')
+        nested_view_1 = self._makeFSPageTemplate('nested_view_1', 'nested_view_1.zpt')
+        nested_view_2 = self._makeFSDTMLMethod('nested_view_2', 'nested_view_2.dtml')
+        portal._setObject('nested_view', nested_view)
+        portal._setObject('nested_view_1', nested_view_1)
+        portal._setObject('nested_view_2', nested_view_2)
+
+        portal.doc1()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_fireForSubtemplates(self):
+        # This is a FSPageTemplate that will be used as the View for 
+        # our content objects. It doesn't matter what it returns.
+        dv = self._makeFSPageTemplate('dummy_view', 'testPT_CPM1.zpt')
+        self.portal._setObject('dummy_view', dv)
+
+        # These are the subtemplates we use
+        sv1 = self._makeFSPageTemplate('subview_1', 'testPT_CPM2.zpt')
+        sv2 = self._makeFSDTMLMethod('subview_2', 'testDTML_CPM3.dtml')
+        self.portal._setObject('subview_1', sv1)
+        self.portal._setObject( 'subview_2', sv2)
+
+        for i in (1,2,3):
+            id = 'doc%i' % i
+            title = 'Document %i' % i
+            description = 'This is document %i' % i
+            modified_date = DateTime()
+            doc = DummyContent(id)
+            doc.title = title
+            doc.description = description
+            doc.modified_date = modified_date
+            self.portal._setObject(id, doc)
+
+        cpm = self.portal.caching_policy_manager
+
+        # This policy only applies to doc2.
+        cpm.addPolicy(policy_id = 'policy_doc2',
+                      predicate = 'python:object.getId()=="doc2"',
+                      mtime_func = '',
+                      max_age_secs = 200,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc2',
+                      etag_func = '',
+                      pre_check=1
+                      )
+
+        # This policy only applies to doc3. 
+        cpm.addPolicy(policy_id = 'policy_doc3',
+                      predicate = 'python:object.getId()=="doc3"',
+                      mtime_func = '',
+                      max_age_secs = 300,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc3',
+                      etag_func = '',
+                      post_check=1
+                      )
+
+        # http://www.zope.org/Collectors/CMF/456
+        # In cases where one view (ZPT or DTML) is rendered from another
+        # view, we want to ensure only the view requested by the visitor
+        # will get caching rules applied.
+        self.portal.doc1.dummy_view()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+        
+    def test_fireForSubtemplates2(self):
+        # This is a FSPageTemplate that will be used as the View for 
+        # our content objects. It doesn't matter what it returns.
+        dv = self._makeFSPageTemplate('dummy_view', 'testPT_CPM1.zpt')
+        self.portal._setObject('dummy_view', dv)
+
+        # These are the subtemplates we use
+        sv1 = self._makeFSPageTemplate('subview_1', 'testPT_CPM2.zpt')
+        sv2 = self._makeFSDTMLMethod('subview_2', 'testDTML_CPM3.dtml')
+        self.portal._setObject('subview_1', sv1)
+        self.portal._setObject( 'subview_2', sv2)
+
+        for i in (1,2,3):
+            id = 'doc%i' % i
+            title = 'Document %i' % i
+            description = 'This is document %i' % i
+            modified_date = DateTime()
+            doc = DummyContent(id)
+            doc.title = title
+            doc.description = description
+            doc.modified_date = modified_date
+            self.portal._setObject(id, doc)
+
+        cpm = self.portal.caching_policy_manager
+
+        # This policy only applies to doc1.
+        cpm.addPolicy(policy_id = 'policy_doc1',
+                      predicate = 'python:object.getId()=="doc1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      s_max_age_secs=100
+                      )
+
+        # This policy only applies to doc2.
+        cpm.addPolicy(policy_id = 'policy_doc2',
+                      predicate = 'python:object.getId()=="doc2"',
+                      mtime_func = '',
+                      max_age_secs = 200,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc2',
+                      etag_func = '',
+                      pre_check=1
+                      )
+
+        # This policy only applies to doc3. 
+        cpm.addPolicy(policy_id = 'policy_doc3',
+                      predicate = 'python:object.getId()=="doc3"',
+                      mtime_func = '',
+                      max_age_secs = 300,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc3',
+                      etag_func = '',
+                      post_check=1
+                      )
+
+        # http://www.zope.org/Collectors/CMF/456
+        # In cases where one view (ZPT or DTML) is rendered from another
+        # view, we want to ensure only the view requested by the visitor
+        # will get caching rules applied.
+        self.portal.doc1.dummy_view()
+
+        # We want to make sure the correct policy (policy_doc1) has fired
+        # Just to be sure, change headers so they are definitely all 
+        # lower-cased
+        headers = {}
+        header_info = self.RESPONSE.headers.items()
+        [headers.__setitem__(x[0].lower(), x[1]) for x in header_info]
+
+        self.failUnless(headers.get('x-cache-headers-set-by'))
+        self.assertEquals(headers.get('vary'), 'doc1')
+        self.assertEquals( headers.get('cache-control')
+                         , 'max-age=100, s-maxage=100'
+                         )
+        
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(CachingPolicyTests),
         unittest.makeSuite(CachingPolicyManagerTests),
         unittest.makeSuite(CachingPolicyManager304Tests),
+        unittest.makeSuite(NestedTemplateTests),
         ))
 
 if __name__ == '__main__':

Modified: CMF/trunk/CMFCore/utils.py
===================================================================
--- CMF/trunk/CMFCore/utils.py	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFCore/utils.py	2006-11-12 17:06:03 UTC (rev 71111)
@@ -51,6 +51,7 @@
 from exceptions import AccessControl_Unauthorized
 from exceptions import NotFound
 
+SUBTEMPLATE = '__SUBTEMPLATE__'
 
 security = ModuleSecurityInfo( 'Products.CMFCore.utils' )
 
@@ -289,6 +290,12 @@
     if REQUEST is None:
         return False
 
+    # check whether we need to suppress subtemplates
+    call_count = getattr(REQUEST, SUBTEMPLATE, 0)
+    setattr(REQUEST, SUBTEMPLATE, call_count+1)
+    if call_count != 0:
+        return False
+
     if_modified_since = REQUEST.get_header('If-Modified-Since', None)
     if_none_match = REQUEST.get_header('If-None-Match', None)
 
@@ -357,9 +364,10 @@
     if content_etag:
         response.setHeader('ETag', content_etag, literal=1)
     response.setStatus(304)
+    delattr(REQUEST, SUBTEMPLATE)
             
     return True
-    
+   
 
 security.declarePrivate('_setCacheHeaders')
 def _setCacheHeaders(obj, extra_context):
@@ -367,6 +375,14 @@
     REQUEST = getattr(obj, 'REQUEST', None)
 
     if REQUEST is not None:
+        call_count = getattr(REQUEST, SUBTEMPLATE, 1) - 1
+        setattr(REQUEST, SUBTEMPLATE, call_count)
+        if call_count != 0:
+           return
+
+        # cleanup
+        delattr(REQUEST, SUBTEMPLATE)
+
         content = aq_parent(obj)
         manager = getToolByName(obj, 'caching_policy_manager', None)
         if manager is not None:

Modified: CMF/trunk/CMFDefault/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFDefault/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFDefault/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup

Modified: CMF/trunk/CMFTopic/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFTopic/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFTopic/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,4 +1,4 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 CMFDefault
 GenericSetup

Modified: CMF/trunk/CMFUid/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/CMFUid/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/CMFUid/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup

Modified: CMF/trunk/DCWorkflow/DEPENDENCIES.txt
===================================================================
--- CMF/trunk/DCWorkflow/DEPENDENCIES.txt	2006-11-12 15:38:40 UTC (rev 71110)
+++ CMF/trunk/DCWorkflow/DEPENDENCIES.txt	2006-11-12 17:06:03 UTC (rev 71111)
@@ -1,3 +1,3 @@
-Zope >= 2.10.0
+Zope >= 2.10.1
 CMFCore
 GenericSetup
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.