[CMF-checkins] SVN: CMF/trunk/ - CMFCore.CachingPolicyManager:
Prevent firing of caching policies
Jens Vagelpohl
jens at dataflake.org
Sun Nov 12 12:06:05 EST 2006
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
More information about the CMF-checkins
mailing list