[CMF-checkins] SVN: CMF/trunk/C Forward port fix to allow disabling
'Last-Modified' header to work around broken Aiyeeee behavior.
Tres Seaver
tseaver at palladion.com
Tue Oct 11 16:16:26 EDT 2005
Log message for revision 39072:
Forward port fix to allow disabling 'Last-Modified' header to work around broken Aiyeeee behavior.
Changed:
U CMF/trunk/CHANGES.txt
U CMF/trunk/CMFCore/CachingPolicyManager.py
U CMF/trunk/CMFCore/dtml/cachingPolicies.dtml
U CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
U CMF/trunk/CMFCore/utils.py
-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt 2005-10-11 20:16:05 UTC (rev 39071)
+++ CMF/trunk/CHANGES.txt 2005-10-11 20:16:25 UTC (rev 39072)
@@ -2,6 +2,12 @@
New Features
+ - CMFCore.CachingPolicyManager: Caching policies can now control all the
+ Cache-Control tokens defined in the HTTP 1.1 spec (s-maxage, public,
+ private, no-transform). When no-cache is enabled, a Pragma: no-cache
+ header is also sent for HTTP 1.0 clients. Thanks go to Geoff Davis
+ for contributing the necessary patches.
+
- ActionsTool: Improved add form for 'CMF Action' objects.
Presettings can now be loaded from Action settings in setup profiles.
Modified: CMF/trunk/CMFCore/CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/CachingPolicyManager.py 2005-10-11 20:16:05 UTC (rev 39071)
+++ CMF/trunk/CMFCore/CachingPolicyManager.py 2005-10-11 20:16:25 UTC (rev 39072)
@@ -89,10 +89,11 @@
'time' -- A DateTime object for the current date and time
- - The "Last-modified" HTTP response header will be set using
- 'mtime_func', which is another TALES expression evaluated
+ - mtime_func is used to set the "Last-modified" HTTP response
+ header, which is another TALES expression evaluated
against the same namespace. If not specified explicitly,
- uses 'object/modified'.
+ uses 'object/modified'. mtime_func is also used in responding
+ to conditional GETs.
- The "Expires" HTTP response header and the "max-age" token of
the "Cache-control" header will be set using 'max_age_secs',
@@ -131,6 +132,25 @@
'private=1' argument => "private" token
'no_transform=1' argument => "no-transform" token
+
+ - The last_modified argument is used to determine whether to add a
+ Last-Modified header. last_modified=1 by default. There appears
+ to be a bug in IE 6 (and possibly other versions) that uses the
+ Last-Modified header plus some heuristics rather than the other
+ explicit caching headers to determine whether to render content
+ from the cache. If you set, say, max-age=0, must-revalidate and
+ have a Last-Modified header some time in the past, IE will
+ recognize that the page in cache is stale and will request an
+ update from the server BUT if you have a Last-Modified header
+ with an older date, will then ignore the update and render from
+ the cache, so you may want to disable the Last-Modified header
+ when controlling caching using Cache-Control headers.
+
+ - The pre-check and post-check Cache-Control tokens are Microsoft
+ proprietary tokens added to IE 5+. Documentation can be found
+ here: http://msdn.microsoft.com/workshop/author/perf/perftips.asp
+ Unfortunately these are needed to make IE behave correctly.
+
"""
def __init__( self
@@ -149,6 +169,9 @@
, private=0
, no_transform=0
, enable_304s=0
+ , last_modified=1
+ , pre_check=None
+ , post_check=None
):
if not predicate:
@@ -158,11 +181,29 @@
mtime_func = 'object/modified'
if max_age_secs is not None:
- max_age_secs = int( max_age_secs )
+ if str(max_age_secs).strip() == '':
+ max_age_secs = None
+ else:
+ max_age_secs = int( max_age_secs )
if s_max_age_secs is not None:
- s_max_age_secs = int( s_max_age_secs )
+ if str(s_max_age_secs).strip() == '':
+ s_max_age_secs = None
+ else:
+ s_max_age_secs = int( s_max_age_secs )
+ if pre_check is not None:
+ if str(pre_check).strip() == '':
+ pre_check = None
+ else:
+ pre_check = int(pre_check)
+
+ if post_check is not None:
+ if str(post_check).strip() == '':
+ post_check = None
+ else:
+ post_check = int(post_check)
+
self._policy_id = policy_id
self._predicate = Expression( text=predicate )
self._mtime_func = Expression( text=mtime_func )
@@ -178,6 +219,9 @@
self._vary = vary
self._etag_func = Expression( text=etag_func )
self._enable_304s = int ( enable_304s )
+ self._last_modified = int( last_modified )
+ self._pre_check = pre_check
+ self._post_check = post_check
def getPolicyId( self ):
"""
@@ -260,6 +304,20 @@
"""
return getattr(self, '_enable_304s', 0)
+ def getLastModified(self):
+ """Should we set the last modified header?"""
+ return getattr(self, '_last_modified', 1)
+
+ def getPreCheck(self):
+ """
+ """
+ return getattr(self, '_pre_check', None)
+
+ def getPostCheck(self):
+ """
+ """
+ return getattr(self, '_post_check', None)
+
def testPredicate(self, expr_context):
""" Does this request match our predicate?"""
return self._predicate(expr_context)
@@ -274,16 +332,14 @@
if self.testPredicate( expr_context ):
- mtime = self._mtime_func( expr_context )
+ if self.getLastModified():
+ mtime = self._mtime_func( expr_context )
+ if type( mtime ) is type( '' ):
+ mtime = DateTime( mtime )
+ if mtime is not None:
+ mtime_str = rfc1123_date(mtime.timeTime())
+ headers.append( ( 'Last-modified', mtime_str ) )
- if type( mtime ) is type( '' ):
- mtime = DateTime( mtime )
-
- if mtime is not None:
- mtime_flt = mtime.timeTime()
- mtime_str = rfc1123_date(mtime_flt)
- headers.append( ( 'Last-modified', mtime_str ) )
-
control = []
if self.getMaxAgeSecs() is not None:
@@ -318,6 +374,14 @@
if self.getNoTransform():
control.append( 'no-transform' )
+ pre_check = self.getPreCheck()
+ if pre_check is not None:
+ control.append('pre-check=%d' % pre_check)
+
+ post_check = self.getPostCheck()
+ if post_check is not None:
+ control.append('post-check=%d' % post_check)
+
if control:
headers.append( ( 'Cache-control', ', '.join( control ) ) )
@@ -394,15 +458,33 @@
, private=0 # boolean (def. 0)
, no_transform=0 # boolean (def. 0)
, enable_304s=0 # boolean (def. 0)
+ , last_modified=1 # boolean (def. 1)
+ , pre_check=None # integer, default None
+ , post_check=None # integer, default None
):
"""
Add a caching policy.
"""
+ if max_age_secs is None or str(max_age_secs).strip() == '':
+ max_age_secs = None
+ else:
+ max_age_secs = int(max_age_secs)
+
if s_max_age_secs is None or str(s_max_age_secs).strip() == '':
s_max_age_secs = None
else:
s_max_age_secs = int(s_max_age_secs)
+
+ if pre_check is None or str(pre_check).strip() == '':
+ pre_check = None
+ else:
+ pre_check = int(pre_check)
+ if post_check is None or str(post_check).strip() == '':
+ post_check = None
+ else:
+ post_check = int(post_check)
+
self._addPolicy( policy_id
, predicate
, mtime_func
@@ -418,6 +500,9 @@
, private
, no_transform
, enable_304s
+ , last_modified
+ , pre_check
+ , post_check
)
if REQUEST is not None:
REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
@@ -444,15 +529,33 @@
, private=0 # boolean (def. 0)
, no_transform=0 # boolean (def. 0)
, enable_304s=0 # boolean (def. 0)
+ , last_modified=1 # boolean (def. 1)
+ , pre_check=0 # integer, default=None
+ , post_check=0 # integer, default=None
):
"""
Update a caching policy.
"""
+ if max_age_secs is None or str(max_age_secs).strip() == '':
+ max_age_secs = None
+ else:
+ max_age_secs = int(max_age_secs)
+
if s_max_age_secs is None or str(s_max_age_secs).strip() == '':
s_max_age_secs = None
else:
s_max_age_secs = int(s_max_age_secs)
+
+ if pre_check is None or str(pre_check).strip() == '':
+ pre_check = None
+ else:
+ pre_check = int(pre_check)
+ if post_check is None or str(post_check).strip() == '':
+ post_check = None
+ else:
+ post_check = int(post_check)
+
self._updatePolicy( policy_id
, predicate
, mtime_func
@@ -468,6 +571,9 @@
, private
, no_transform
, enable_304s
+ , last_modified
+ , pre_check
+ , post_check
)
if REQUEST is not None:
REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
@@ -544,6 +650,9 @@
, private=0
, no_transform=0
, enable_304s=0
+ , last_modified=1
+ , pre_check=None
+ , post_check=None
):
"""
Add a policy to our registry.
@@ -571,6 +680,9 @@
, private
, no_transform
, enable_304s
+ , last_modified
+ , pre_check
+ , post_check
)
idlist = list( self._policy_ids )
idlist.append( policy_id )
@@ -593,6 +705,9 @@
, private=0
, no_transform=0
, enable_304s=0
+ , last_modified=1
+ , pre_check=None
+ , post_check=None
):
"""
Update a policy in our registry.
@@ -615,6 +730,9 @@
, private
, no_transform
, enable_304s
+ , last_modified
+ , pre_check
+ , post_check
)
security.declarePrivate( '_reorderPolicy' )
@@ -668,28 +786,27 @@
security.declareProtected( View, 'getModTimeAndETag' )
def getModTimeAndETag( self, content, view_method, keywords, time=None):
""" Return the modification time and ETag for the content object,
- view method, and keywords as the tuple (modification_time, etag)
- (where modification_time is a DateTime) or None.
+ view method, and keywords as the tuple (modification_time, etag,
+ set_last_modified_header), where modification_time is a DateTime,
+ or None.
"""
context = createCPContext( content, view_method, keywords, time=time )
for policy_id, policy in self.listPolicies():
if policy.getEnable304s() and policy.testPredicate(context):
- headers = policy.getHeaders(context)
- if headers:
- content_etag = None
- content_mod_time = None
- for key, value in headers:
- lk = key.lower()
- if lk == 'etag':
- content_etag = value
- elif lk == 'last-modified':
- last_modified = DateTime(value)
- return (last_modified, content_etag)
+
+ last_modified = policy._mtime_func(context)
+ if type(last_modified) is type(''):
+ last_modified = DateTime(last_modified)
+
+ content_etag = None
+ if policy.getETagFunc():
+ content_etag = policy._etag_func(context)
+
+ return (last_modified, content_etag, policy.getLastModified())
+
return None
-
-
InitializeClass( CachingPolicyManager )
Modified: CMF/trunk/CMFCore/dtml/cachingPolicies.dtml
===================================================================
--- CMF/trunk/CMFCore/dtml/cachingPolicies.dtml 2005-10-11 20:16:05 UTC (rev 39071)
+++ CMF/trunk/CMFCore/dtml/cachingPolicies.dtml 2005-10-11 20:16:25 UTC (rev 39072)
@@ -24,7 +24,10 @@
priv_checked="getPrivate() and 'checked' or ''"
nt_checked="getNoTransform() and 'checked' or ''"
e304_checked="getEnable304s() and 'checked' or ''"
- s_max_age_secs="getSMaxAgeSecs() is not None and getSMaxAgeSecs() or ''">
+ s_max_age_secs="getSMaxAgeSecs() is not None and getSMaxAgeSecs() or ''"
+ last_modified_checked="getLastModified() and 'checked' or ''"
+ pre_check="test(getPreCheck() is None, '', getPreCheck())"
+ post_check="test(getPostCheck() is None, '', getPostCheck())">
<input type="hidden" name="policy_id" value="&dtml-getPolicyId;">
<input type="hidden" name="no_cache:default:int" value="0">
@@ -35,6 +38,7 @@
<input type="hidden" name="private:default:int" value="0">
<input type="hidden" name="no_transform:default:int" value="0">
<input type="hidden" name="enable_304s:default:int" value="0">
+ <input type="hidden" name="last_modified:default:int" value="0">
<table>
@@ -54,11 +58,7 @@
size="40">
</td>
- <th align="right"> No-cache? </th>
- <td>
- <input type="checkbox" name="no_cache:int"
- value="1" &dtml-nc_checked;>
- </td>
+ <td colspan="2"> </td>
</tr>
<tr valign="top">
@@ -70,6 +70,21 @@
size="40">
</td>
+ <th align="right">Last-Modified?</th>
+ <td>
+ <input type="checkbox"
+ name="last_modified:int"
+ value="1" &dtml-last_modified_checked;>
+ </td>
+ </tr>
+
+ <tr valign="top">
+ <th align="right"> No-cache? </th>
+ <td>
+ <input type="checkbox" name="no_cache:int"
+ value="1" &dtml-nc_checked;>
+ </td>
+
<th align="right"> No-store? </th>
<td>
<input type="checkbox" name="no_store:int"
@@ -108,6 +123,22 @@
</tr>
<tr valign="top">
+ <th align="right"> Pre-check (secs) </th>
+ <td>
+ <input type="text"
+ name="pre_check"
+ value="&dtml-pre_check;">
+ </td>
+
+ <th align="right"> Post-check (secs) </th>
+ <td>
+ <input type="text"
+ name="post_check"
+ value="&dtml-post_check;">
+ </td>
+ </tr>
+
+ <tr valign="top">
<th align="right"> Vary </th>
<td>
<input type="text"
@@ -185,6 +216,7 @@
<td align="left">
<form action="&dtml-absolute_url;">
+ <input type="hidden" name="last_modified:default:int" value="0">
<input type="hidden" name="no_cache:default:int" value="0">
<input type="hidden" name="no_store:default:int" value="0">
<input type="hidden" name="must_revalidate:default:int" value="0">
@@ -209,10 +241,7 @@
<input type="text" name="predicate" size="40">
</td>
- <th align="right"> No-cache? </th>
- <td>
- <input type="checkbox" name="no_cache:int" value="1">
- </td>
+ <td colspan="2"> </td>
</tr>
<tr valign="top">
@@ -221,6 +250,18 @@
<input type="text" name="mtime_func" size="40">
</td>
+ <th align="right"> Last-Modified? </th>
+ <td>
+ <input type="checkbox" name="last_modified:int" value="1">
+ </td>
+ </tr>
+
+ <tr>
+ <th align="right"> No-cache? </th>
+ <td>
+ <input type="checkbox" name="no_cache:int" value="1">
+ </td>
+
<th align="right"> No-store? </th>
<td>
<input type="checkbox" name="no_store:int" value="1">
@@ -256,6 +297,22 @@
</tr>
<tr valign="top">
+ <th align="right"> Pre-check (secs) </th>
+ <td>
+ <input type="text"
+ name="pre_check"
+ value="">
+ </td>
+
+ <th align="right"> Post-check (secs) </th>
+ <td>
+ <input type="text"
+ name="post_check"
+ value="">
+ </td>
+ </tr>
+
+ <tr valign="top">
<th align="right"> Vary </th>
<td>
<input type="text" name="vary" size="40">
Modified: CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py 2005-10-11 20:16:05 UTC (rev 39071)
+++ CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py 2005-10-11 20:16:25 UTC (rev 39072)
@@ -71,7 +71,6 @@
, 'foo_view', kw, self._epoch )
def test_empty( self ):
-
policy = self._makePolicy( 'empty' )
context = self._makeContext()
headers = policy.getHeaders( context )
@@ -304,6 +303,45 @@
self.assertEqual( headers[1][1] , 'no-transform' )
self.assertEqual(policy.getNoTransform(), 1)
+ def test_lastModified( self ):
+
+ policy = self._makePolicy( 'lastModified', last_modified=0 )
+ context = self._makeContext()
+ headers = policy.getHeaders( context )
+
+ self.assertEqual( len( headers ), 0 )
+ self.assertEqual(policy.getLastModified(), 0)
+
+ def test_preCheck( self ):
+
+ policy = self._makePolicy( 'preCheck', pre_check=1 )
+ context = self._makeContext()
+ headers = policy.getHeaders( context )
+
+ self.assertEqual( len( headers ), 2 )
+ self.assertEqual( headers[0][0].lower() , 'last-modified' )
+ self.assertEqual( headers[0][1]
+ , rfc1123_date(self._epoch.timeTime()) )
+ self.assertEqual( headers[1][0].lower() , 'cache-control' )
+ self.assertEqual( headers[1][1] , 'pre-check=1' )
+ self.assertEqual(policy.getPreCheck(), 1)
+ self.assertEqual(policy.getPostCheck(), None)
+
+ def test_postCheck( self ):
+
+ policy = self._makePolicy( 'postCheck', post_check=1 )
+ context = self._makeContext()
+ headers = policy.getHeaders( context )
+
+ self.assertEqual( len( headers ), 2 )
+ self.assertEqual( headers[0][0].lower() , 'last-modified' )
+ self.assertEqual( headers[0][1]
+ , rfc1123_date(self._epoch.timeTime()) )
+ self.assertEqual( headers[1][0].lower() , 'cache-control' )
+ self.assertEqual( headers[1][1] , 'post-check=1' )
+ self.assertEqual(policy.getPostCheck(), 1)
+ self.assertEqual(policy.getPreCheck(), None)
+
def test_ETag( self ):
# With an empty etag_func, no ETag should be produced
@@ -392,7 +430,7 @@
def test_addAndUpdatePolicy( self ):
mgr = self._makeOne()
- mgr.addPolicy( 'first', 'python:1', 'mtime', 1, 0, 1, 0, 'vary', 'etag', None, 2, 1, 0, 1, 0 )
+ mgr.addPolicy( 'first', 'python:1', 'mtime', 1, 0, 1, 0, 'vary', 'etag', None, 2, 1, 0, 1, 0, 1, 0, 2, 3 )
p = mgr._policies['first']
self.assertEqual(p.getPolicyId(), 'first')
self.assertEqual(p.getPredicate(), 'python:1')
@@ -408,8 +446,12 @@
self.assertEqual(p.getPublic(), 0)
self.assertEqual(p.getPrivate(), 1)
self.assertEqual(p.getNoTransform(), 0)
+ self.assertEqual(p.getEnable304s(), 1)
+ self.assertEqual(p.getLastModified(), 0)
+ self.assertEqual(p.getPreCheck(), 2)
+ self.assertEqual(p.getPostCheck(), 3)
- mgr.updatePolicy( 'first', 'python:0', 'mtime2', 2, 1, 0, 1, 'vary2', 'etag2', None, 1, 0, 1, 0, 1 )
+ mgr.updatePolicy( 'first', 'python:0', 'mtime2', 2, 1, 0, 1, 'vary2', 'etag2', None, 1, 0, 1, 0, 1, 0, 1, 3, 2 )
p = mgr._policies['first']
self.assertEqual(p.getPolicyId(), 'first')
self.assertEqual(p.getPredicate(), 'python:0')
@@ -425,6 +467,10 @@
self.assertEqual(p.getPublic(), 1)
self.assertEqual(p.getPrivate(), 0)
self.assertEqual(p.getNoTransform(), 1)
+ self.assertEqual(p.getEnable304s(), 0)
+ self.assertEqual(p.getLastModified(), 1)
+ self.assertEqual(p.getPreCheck(), 3)
+ self.assertEqual(p.getPostCheck(), 2)
def test_reorder( self ):
@@ -690,8 +736,8 @@
doc1()
self.assertEqual(response.getStatus(), 200)
self._cleanup()
-
+
def testConditionalGETETag(self):
yesterday = DateTime() - 1
doc2 = self.portal.doc2
Modified: CMF/trunk/CMFCore/utils.py
===================================================================
--- CMF/trunk/CMFCore/utils.py 2005-10-11 20:16:05 UTC (rev 39071)
+++ CMF/trunk/CMFCore/utils.py 2005-10-11 20:16:25 UTC (rev 39072)
@@ -352,9 +352,9 @@
# no appropriate policy or 304s not enabled
return False
- (content_mod_time, content_etag) = ret
+ (content_mod_time, content_etag, set_last_modified_header) = ret
if content_mod_time:
- mod_time_secs = content_mod_time.timeTime()
+ mod_time_secs = long(content_mod_time.timeTime())
else:
mod_time_secs = None
@@ -399,7 +399,7 @@
return False
response = REQUEST.RESPONSE
- if content_mod_time:
+ if content_mod_time and set_last_modified_header:
response.setHeader('Last-modified', str(content_mod_time))
if content_etag:
response.setHeader('ETag', content_etag, literal=1)
More information about the CMF-checkins
mailing list