[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">&nbsp;</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">&nbsp;</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