[CMF-checkins] SVN: CMF/trunk/CMFCore/ - CMFCore.CachingPolicyManager: Caching policies can now control all the

Jens Vagelpohl jens at dataflake.org
Tue Sep 13 14:15:43 EDT 2005


Log message for revision 38458:
  - 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.
  
  - CMFCore.FSPageTemplate:  These now have the option to return a status 
    304 and no content when a conditional GET is sent.  This is a potential
    significant win for sites. This behavior is activated on the 
    CachingPolicyManager by using policies that set suitable ETag headers
    when delivering the content. Geoff Davis invested the time to add 
    this feature.
  

Changed:
  U   CMF/trunk/CMFCore/CachingPolicyManager.py
  U   CMF/trunk/CMFCore/FSPageTemplate.py
  U   CMF/trunk/CMFCore/dtml/cachingPolicies.dtml
  U   CMF/trunk/CMFCore/tests/base/dummy.py
  U   CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
  U   CMF/trunk/CMFCore/utils.py

-=-
Modified: CMF/trunk/CMFCore/CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/CachingPolicyManager.py	2005-09-13 18:15:07 UTC (rev 38457)
+++ CMF/trunk/CMFCore/CachingPolicyManager.py	2005-09-13 18:15:42 UTC (rev 38458)
@@ -96,6 +96,10 @@
             the "Cache-control" header will be set using 'max_age_secs',
             if passed;  it should be an integer value in seconds.
 
+          - The "s-maxage" token of the "Cache-control" header will be
+            set using 's_max_age_secs', if passed;  it should be an integer
+            value in seconds.
+
           - The "Vary" HTTP response headers will be set if a value is 
             provided. The Vary header is described in RFC 2616. In essence,
             it instructs caches that respect this header (such as Squid
@@ -117,6 +121,14 @@
              'no_store=1' argument => "no-store" token
 
              'must_revalidate=1' argument => "must-revalidate" token
+
+             'proxy_revalidate=1' argument => "proxy-revalidate" token
+             
+             'public=1' argument => "public" token
+             
+             'private=1' argument => "private" token
+
+             'no_transform=1' argument => "no-transform" token
     """
 
     def __init__( self
@@ -129,6 +141,12 @@
                 , must_revalidate=0
                 , vary=''
                 , etag_func=''
+                , s_max_age_secs=None
+                , proxy_revalidate=0
+                , public=0
+                , private=0
+                , no_transform=0
+                , enable_304s=0
                 ):
 
         if not predicate:
@@ -140,15 +158,24 @@
         if max_age_secs is not None:
             max_age_secs = int( max_age_secs )
 
+        if s_max_age_secs is not None:
+            s_max_age_secs = int( s_max_age_secs )
+
         self._policy_id = policy_id
         self._predicate = Expression( text=predicate )
         self._mtime_func = Expression( text=mtime_func )
         self._max_age_secs = max_age_secs
+        self._s_max_age_secs = s_max_age_secs
         self._no_cache = int( no_cache )
         self._no_store = int( no_store )
         self._must_revalidate = int( must_revalidate )
+        self._proxy_revalidate = int( proxy_revalidate )
+        self._public = int( public )
+        self._private = int( private )
+        self._no_transform = int( no_transform )
         self._vary = vary
         self._etag_func = Expression( text=etag_func )
+        self._enable_304s = int ( enable_304s )
 
     def getPolicyId( self ):
         """
@@ -170,6 +197,11 @@
         """
         return self._max_age_secs
 
+    def getSMaxAgeSecs( self ):
+        """
+        """
+        return getattr(self, '_s_max_age_secs', None)
+
     def getNoCache( self ):
         """
         """
@@ -185,6 +217,26 @@
         """
         return self._must_revalidate
 
+    def getProxyRevalidate( self ):
+        """
+        """
+        return getattr(self, '_proxy_revalidate', 0)
+
+    def getPublic( self ):
+        """
+        """
+        return getattr(self, '_public', 0)
+
+    def getPrivate( self ):
+        """
+        """
+        return getattr(self, '_private', 0)
+
+    def getNoTransform( self ):
+        """
+        """
+        return getattr(self, '_no_transform', 0)
+
     def getVary( self ):
         """
         """
@@ -201,6 +253,15 @@
 
         return etag_func_text
 
+    def getEnable304s(self):
+        """
+        """
+        return getattr(self, '_enable_304s', 0)
+
+    def testPredicate(self, expr_context):
+        """ Does this request match our predicate?"""
+        return self._predicate(expr_context)
+
     def getHeaders( self, expr_context ):
         """
             Does this request match our predicate?  If so, return a
@@ -209,7 +270,7 @@
         """
         headers = []
 
-        if self._predicate( expr_context ):
+        if self.testPredicate( expr_context ):
 
             mtime = self._mtime_func( expr_context )
 
@@ -223,21 +284,38 @@
 
             control = []
 
-            if self._max_age_secs is not None:
+            if self.getMaxAgeSecs() is not None:
                 now = expr_context.vars[ 'time' ]
                 exp_time_str = rfc1123_date(now.timeTime() + self._max_age_secs)
                 headers.append( ( 'Expires', exp_time_str ) )
                 control.append( 'max-age=%d' % self._max_age_secs )
+                
+            if self.getSMaxAgeSecs() is not None:
+                control.append( 's-maxage=%d' % self._s_max_age_secs )
 
-            if self._no_cache:
+            if self.getNoCache():
                 control.append( 'no-cache' )
+                # The following is for HTTP 1.0 clients
+                headers.append(('Pragma', 'no-cache'))
 
-            if self._no_store:
+            if self.getNoStore():
                 control.append( 'no-store' )
 
-            if self._must_revalidate:
+            if self.getPublic():
+                control.append( 'public' )
+
+            if self.getPrivate():
+                control.append( 'private' )
+
+            if self.getMustRevalidate():
                 control.append( 'must-revalidate' )
 
+            if self.getProxyRevalidate():
+                control.append( 'proxy-revalidate' )
+
+            if self.getNoTransform():
+                control.append( 'no-transform' )
+
             if control:
                 headers.append( ( 'Cache-control', ', '.join( control ) ) )
 
@@ -250,6 +328,7 @@
         return headers
 
 
+
 class CachingPolicyManager( SimpleItem ):
     """
         Manage the set of CachingPolicy objects for the site;  dispatch
@@ -297,19 +376,30 @@
     security.declareProtected( ManagePortal, 'addPolicy' )
     def addPolicy( self
                  , policy_id
-                 , predicate        # TALES expr (def. 'python:1')
-                 , mtime_func       # TALES expr (def. 'object/modified')
-                 , max_age_secs     # integer, seconds (def. 0)
-                 , no_cache         # boolean (def. 0)
-                 , no_store         # boolean (def. 0)
-                 , must_revalidate  # boolean (def. 0)
-                 , vary             # string value
-                 , etag_func        # TALES expr (def. '')
+                 , predicate           # TALES expr (def. 'python:1')
+                 , mtime_func          # TALES expr (def. 'object/modified')
+                 , max_age_secs        # integer, seconds (def. 0)
+                 , no_cache            # boolean (def. 0)
+                 , no_store            # boolean (def. 0)
+                 , must_revalidate     # boolean (def. 0)
+                 , vary                # string value
+                 , etag_func           # TALES expr (def. '')
                  , REQUEST=None
+                 , s_max_age_secs=None # integer, seconds (def. None)
+                 , proxy_revalidate=0  # boolean (def. 0)
+                 , public=0            # boolean (def. 0)
+                 , private=0           # boolean (def. 0)
+                 , no_transform=0      # boolean (def. 0)
+                 , enable_304s=0       # boolean (def. 0)
                  ):
         """
             Add a caching policy.
         """
+        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)
+
         self._addPolicy( policy_id
                        , predicate
                        , mtime_func
@@ -319,6 +409,12 @@
                        , must_revalidate
                        , vary
                        , etag_func
+                       , s_max_age_secs
+                       , proxy_revalidate
+                       , public
+                       , private
+                       , no_transform
+                       , enable_304s
                        )
         if REQUEST is not None: 
             REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
@@ -330,19 +426,30 @@
     security.declareProtected( ManagePortal, 'updatePolicy' )
     def updatePolicy( self
                     , policy_id
-                    , predicate         # TALES expr (def. 'python:1')
-                    , mtime_func        # TALES expr (def. 'object/modified')
-                    , max_age_secs      # integer, seconds
-                    , no_cache          # boolean (def. 0)
-                    , no_store          # boolean (def. 0)
-                    , must_revalidate   # boolean (def. 0)
-                    , vary              # string value
-                    , etag_func         # TALES expr (def. '')
+                    , predicate           # TALES expr (def. 'python:1')
+                    , mtime_func          # TALES expr (def. 'object/modified')
+                    , max_age_secs        # integer, seconds (def. 0)
+                    , no_cache            # boolean (def. 0)
+                    , no_store            # boolean (def. 0)
+                    , must_revalidate     # boolean (def. 0)
+                    , vary                # string value
+                    , etag_func           # TALES expr (def. '')
                     , REQUEST=None
+                    , s_max_age_secs=None # integer, seconds (def. 0)
+                    , proxy_revalidate=0  # boolean (def. 0)
+                    , public=0            # boolean (def. 0)
+                    , private=0           # boolean (def. 0)
+                    , no_transform=0      # boolean (def. 0)
+                    , enable_304s=0       # boolean (def. 0)
                     ):
         """
             Update a caching policy.
         """
+        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)
+
         self._updatePolicy( policy_id
                           , predicate
                           , mtime_func
@@ -352,6 +459,12 @@
                           , must_revalidate
                           , vary
                           , etag_func
+                          , s_max_age_secs
+                          , proxy_revalidate
+                          , public
+                          , private
+                          , no_transform
+                          , enable_304s
                           )
         if REQUEST is not None: 
             REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
@@ -422,6 +535,12 @@
                   , must_revalidate
                   , vary
                   , etag_func
+                  , s_max_age_secs=None
+                  , proxy_revalidate=0
+                  , public=0
+                  , private=0
+                  , no_transform=0
+                  , enable_304s=0
                   ):
         """
             Add a policy to our registry.
@@ -443,6 +562,12 @@
                                                    , must_revalidate
                                                    , vary
                                                    , etag_func
+                                                   , s_max_age_secs
+                                                   , proxy_revalidate
+                                                   , public
+                                                   , private
+                                                   , no_transform
+                                                   , enable_304s
                                                    )
         idlist = list( self._policy_ids )
         idlist.append( policy_id )
@@ -459,6 +584,12 @@
                      , must_revalidate
                      , vary
                      , etag_func
+                     , s_max_age_secs=None
+                     , proxy_revalidate=0
+                     , public=0
+                     , private=0
+                     , no_transform=0
+                     , enable_304s=0
                      ):
         """
             Update a policy in our registry.
@@ -475,6 +606,12 @@
                                                    , must_revalidate
                                                    , vary
                                                    , etag_func
+                                                   , s_max_age_secs
+                                                   , proxy_revalidate
+                                                   , public
+                                                   , private
+                                                   , no_transform
+                                                   , enable_304s
                                                    )
 
     security.declarePrivate( '_reorderPolicy' )
@@ -525,6 +662,31 @@
 
         return ()
 
+    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.
+        """
+        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)
+        return None
+
+
+
+
 InitializeClass( CachingPolicyManager )
 
 

Modified: CMF/trunk/CMFCore/FSPageTemplate.py
===================================================================
--- CMF/trunk/CMFCore/FSPageTemplate.py	2005-09-13 18:15:07 UTC (rev 38457)
+++ CMF/trunk/CMFCore/FSPageTemplate.py	2005-09-13 18:15:42 UTC (rev 38458)
@@ -30,7 +30,7 @@
 from permissions import FTPAccess
 from permissions import View
 from permissions import ViewManagementScreens
-from utils import _setCacheHeaders
+from utils import _setCacheHeaders, _checkConditionalGET
 from utils import expandpath
 
 xml_detect_re = re.compile('^\s*<\?xml\s+(?:[^>]*?encoding=["\']([^"\'>]+))?')
@@ -125,6 +125,13 @@
 
     def pt_render(self, source=0, extra_context={}):
         self._updateFromFS()  # Make sure the template has been loaded.
+
+        if not source:
+            # If we have a conditional get, set status 304 and return
+            # no content
+            if _checkConditionalGET(self, extra_context):
+                return ''
+        
         result = FSPageTemplate.inheritedAttribute('pt_render')(
                                 self, source, extra_context
                                 )

Modified: CMF/trunk/CMFCore/dtml/cachingPolicies.dtml
===================================================================
--- CMF/trunk/CMFCore/dtml/cachingPolicies.dtml	2005-09-13 18:15:07 UTC (rev 38457)
+++ CMF/trunk/CMFCore/dtml/cachingPolicies.dtml	2005-09-13 18:15:42 UTC (rev 38458)
@@ -18,12 +18,23 @@
      <dtml-with policy>
       <dtml-let nc_checked="getNoCache() and 'checked' or ''"
                 ns_checked="getNoStore() and 'checked' or ''"
-                mr_checked="getMustRevalidate() and 'checked' or ''">
+                mr_checked="getMustRevalidate() and 'checked' or ''"
+                pr_checked="getProxyRevalidate() and 'checked' or ''"
+                pub_checked="getPublic() and 'checked' or ''"
+                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 ''">
 
        <input type="hidden" name="policy_id" value="&dtml-getPolicyId;">
        <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">
+       <input type="hidden" name="proxy_revalidate:default:int" value="0">
+       <input type="hidden" name="public:default:int" value="0">
+       <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">
 
        <table>
 
@@ -82,26 +93,64 @@
        </tr>
 
        <tr valign="top">
+       <th align="right"> Max proxy cache age (secs) </th>
+       <td>
+         <input type="text"
+                name="s_max_age_secs"
+               value="&dtml-s_max_age_secs;">
+       </td>
+
+       <th align="right"> Proxy-revalidate? </th>
+       <td>
+         <input type="checkbox" name="proxy_revalidate:int"
+                                value="1" &dtml-pr_checked;>
+       </td>
+       </tr>
+
+       <tr valign="top">
        <th align="right"> Vary </th>
-       <td colspan="3">
+       <td>
          <input type="text"
                 name="vary"
                 value="&dtml-getVary;"
                 size="40">
        </td>
+       <th align="right"> Public? </th>
+       <td>
+         <input type="checkbox" name="public:int"
+                                value="1" &dtml-pub_checked;>
+       </td>
        </tr>
 
        <tr valign="top">
        <th align="right"> ETag </th>
-       <td colspan="3">
+       <td>
          <input type="text"
                 name="etag_func"
                 value="&dtml-getETagFunc;"
                 size="40">
        </td>
+       <th align="right"> Private? </th>
+       <td>
+         <input type="checkbox" name="private:int"
+                                value="1" &dtml-priv_checked;>
+       </td>
        </tr>
 
+       <tr valign="top">
+       <th align="right"> Enable 304s? </th>
+       <td>
+         <input type="checkbox" name="enable_304s:int"
+                                value="1" &dtml-e304_checked;>
+       </td>
+       <th align="right"> No-transform? </th>
+       <td>
+         <input type="checkbox" name="no_transform:int"
+                                value="1" &dtml-nt_checked;>
+       </td>
+       </tr>
 
+
        <tr valign="top">
        <td><br /></td>
        <td colspan="3">
@@ -139,6 +188,11 @@
        <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">
+       <input type="hidden" name="proxy_revalidate:default:int" value="0">
+       <input type="hidden" name="public:default:int" value="0">
+       <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">
 
        <table>
 
@@ -187,20 +241,58 @@
        </tr>
 
        <tr valign="top">
+       <th align="right"> Max proxy cache age (secs) </th>
+       <td>
+         <input type="text"
+                name="s_max_age_secs"
+                value="">
+       </td>
+
+       <th align="right"> Proxy-revalidate? </th>
+       <td>
+         <input type="checkbox" name="proxy_revalidate:int"
+                                value="1">
+       </td>
+       </tr>
+
+       <tr valign="top">
        <th align="right"> Vary </th>
-       <td colspan="3">
+       <td>
          <input type="text" name="vary" size="40">
        </td>
+       <th align="right"> Public? </th>
+       <td>
+         <input type="checkbox" name="public:int"
+                                value="1">
+       </td>
        </tr>
 
        <tr valign="top">
        <th align="right"> ETag </th>
-       <td colspan="3">
+       <td>
          <input type="text" name="etag_func" size="40">
        </td>
+       <th align="right"> Private? </th>
+       <td>
+         <input type="checkbox" name="private:int"
+                                value="1">
+       </td>
        </tr>
 
        <tr valign="top">
+       <th align="right"> Enable 304s? </th>
+       <td>
+         <input type="checkbox" name="enable_304s:int"
+                                value="1">
+       </td>
+       <th align="right"> No-transform? </th>
+       <td>
+         <input type="checkbox" name="no_transform:int"
+                                value="1">
+       </td>
+       </tr>
+
+       <tr valign="top">
        <td><br /></td>
        <td>
          <input type="submit" name="addPolicy:method" value=" Add ">

Modified: CMF/trunk/CMFCore/tests/base/dummy.py
===================================================================
--- CMF/trunk/CMFCore/tests/base/dummy.py	2005-09-13 18:15:07 UTC (rev 38457)
+++ CMF/trunk/CMFCore/tests/base/dummy.py	2005-09-13 18:15:42 UTC (rev 38458)
@@ -49,6 +49,32 @@
         return self._id
 
 
+class DummyType(DummyObject):
+    """ A Dummy Type object """
+
+    def __init__(self, id='Dummy Content', title='Dummy Content', actions=()):
+        """ To fake out some actions, pass in a sequence of tuples where the
+        first element represents the ID or alias of the action and the
+        second element is the path to the object to be invoked, such as 
+        a page template.
+        """
+
+        self.id = id
+        self.title = title
+        self._actions = {}
+
+        self._setActions(actions)
+
+    def _setActions(self, actions=()):
+        for action_id, action_path in actions:
+            self._actions[action_id] = action_path
+
+    def Title(self):
+        return self.title
+
+    def queryMethodID(self, alias, default=None, context=None):
+        return self._actions.get(alias, default)
+
 class DummyContent( PortalContent, Item ):
     """
     A Dummy piece of PortalContent
@@ -277,12 +303,14 @@
     Action Provider
     """
 
-    _actions = ( DummyObject()
-               , DummyObject()
-               )
-
     root = 'DummyTool'
 
+    view_actions = ( ('', 'dummy_view')
+                   , ('view', 'dummy_view')
+                   , ('(Default)', 'dummy_view')
+                   )
+
+
     def __init__(self, anon=1):
         self.anon = anon
 
@@ -309,10 +337,12 @@
 
     # TypesTool
     def listTypeInfo(self, container=None):
-        return ( DummyObject('Dummy Content'), )
+        typ = 'Dummy Content'
+        return ( DummyType(typ, title=typ, actions=self.view_actions), )
 
     def getTypeInfo(self, contentType):
-        return ( DummyObject('Dummy Content'), )
+        typ = 'Dummy Content'
+        return DummyType(typ, title=typ, actions=self.view_actions)
 
     # WorkflowTool
     test_notified = None

Modified: CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2005-09-13 18:15:07 UTC (rev 38457)
+++ CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2005-09-13 18:15:42 UTC (rev 38458)
@@ -14,6 +14,8 @@
 
 $Id$
 """
+import base64
+import os
 
 from unittest import TestCase, TestSuite, makeSuite, main
 import Testing
@@ -22,11 +24,24 @@
 
 from App.Common import rfc1123_date
 from DateTime.DateTime import DateTime
+from AccessControl.SecurityManagement import newSecurityManager
 
+from Products.CMFCore.tests.base.testcase import RequestTest
+from Products.CMFCore.tests.base.testcase import FSDVTest
+from Products.CMFCore.tests.test_FSPageTemplate import FSPTMaker
+from Products.CMFCore.tests.base.dummy import DummySite
+from Products.CMFCore.tests.base.dummy import DummyUserFolder
+from Products.CMFCore.tests.base.dummy import DummyContent
+from Products.CMFCore.tests.base.dummy import DummyTool
+from Products.CMFCore.FSPageTemplate import FSPageTemplate
+from Products.CMFCore import CachingPolicyManager
+
+
 ACCLARK = DateTime( '2001/01/01' )
+portal_owner = 'portal_owner'
 
 
-class DummyContent:
+class DummyContent2:
 
     __allow_access_to_unprotected_subobjects__ = 1
 
@@ -52,7 +67,7 @@
 
     def _makeContext( self, **kw ):
         from Products.CMFCore.CachingPolicyManager import createCPContext
-        return createCPContext( DummyContent(self._epoch)
+        return createCPContext( DummyContent2(self._epoch)
                               , 'foo_view', kw, self._epoch )
 
     def test_empty( self ):
@@ -178,18 +193,34 @@
         self.assertEqual( headers[2][0].lower() , 'cache-control' )
         self.assertEqual( headers[2][1] , 'max-age=86400' )
 
+    def test_sMaxAge( self ):
+
+        policy = self._makePolicy( 's_aged', s_max_age_secs=86400 )
+        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] , 's-maxage=86400' )
+        self.assertEqual(policy.getSMaxAgeSecs(), 86400)
+
     def test_noCache( self ):
 
         policy = self._makePolicy( 'noCache', no_cache=1 )
         context = self._makeContext()
         headers = policy.getHeaders( context )
 
-        self.assertEqual( len( headers ), 2 )
+        self.assertEqual( len( headers ), 3 )
         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][0].lower() , 'pragma' )
         self.assertEqual( headers[1][1] , 'no-cache' )
+        self.assertEqual( headers[2][0].lower() , 'cache-control' )
+        self.assertEqual( headers[2][1] , 'no-cache' )
 
     def test_noStore( self ):
 
@@ -217,6 +248,62 @@
         self.assertEqual( headers[1][0].lower() , 'cache-control' )
         self.assertEqual( headers[1][1] , 'must-revalidate' )
 
+    def test_proxyRevalidate( self ):
+
+        policy = self._makePolicy( 'proxyRevalidate', proxy_revalidate=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] , 'proxy-revalidate' )
+        self.assertEqual(policy.getProxyRevalidate(), 1)
+
+    def test_public( self ):
+
+        policy = self._makePolicy( 'public', public=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] , 'public' )
+        self.assertEqual(policy.getPublic(), 1)
+
+    def test_private( self ):
+
+        policy = self._makePolicy( 'private', private=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] , 'private' )
+        self.assertEqual(policy.getPrivate(), 1)
+
+    def test_noTransform( self ):
+
+        policy = self._makePolicy( 'noTransform', no_transform=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] , 'no-transform' )
+        self.assertEqual(policy.getNoTransform(), 1)
+
     def test_ETag( self ):
 
         # With an empty etag_func, no ETag should be produced
@@ -246,12 +333,14 @@
         context = self._makeContext()
         headers = policy.getHeaders( context )
 
-        self.assertEqual( len( headers ), 2 )
+        self.assertEqual( len( headers ), 3 )
         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] , 'no-cache, no-store' )
+        self.assertEqual( headers[1][0].lower() , 'pragma' )
+        self.assertEqual( headers[1][1] , 'no-cache' )
+        self.assertEqual( headers[2][0].lower() , 'cache-control' )
+        self.assertEqual( headers[2][1] , 'no-cache, no-store' )
 
 
 class CachingPolicyManagerTests(TestCase):
@@ -288,7 +377,7 @@
         mgr = self._makeOne()
 
         self.assertEqual( len( mgr.listPolicies() ), 0 )
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
+        headers = mgr.getHTTPCachingHeaders( content=DummyContent2(self._epoch)
                                            , view_method='foo_view'
                                            , keywords={}
                                            , time=self._epoch
@@ -296,28 +385,46 @@
         self.assertEqual( len( headers ), 0 )
 
         self.assertRaises( KeyError, mgr._updatePolicy
-                         , 'xyzzy', None, None, None, None, None, None, '', '' )
+                         , 'xyzzy', None, None, None, None, None, None, '', '', None, None, None, None, None )
         self.assertRaises( KeyError, mgr._removePolicy, 'xyzzy' )
         self.assertRaises( KeyError, mgr._reorderPolicy, 'xyzzy', -1 )
 
-    def test_addPolicy( self ):
+    def test_addAndUpdatePolicy( self ):
 
         mgr = self._makeOne()
-        mgr._addPolicy( 'first', 'python:1', None, 0, 0, 0, 0, '', '' )
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
-                                           , view_method='foo_view'
-                                           , keywords={}
-                                           , time=self._epoch
-                                           )
-        self.assertEqual( len( headers ), 3 )
-        self.assertEqual( headers[0][0].lower() , 'last-modified' )
-        self.assertEqual( headers[0][1]
-                        , rfc1123_date(self._epoch.timeTime()) )
-        self.assertEqual( headers[1][0].lower() , 'expires' )
-        self.assertEqual( headers[1][1]
-                        , rfc1123_date(self._epoch.timeTime()) )
-        self.assertEqual( headers[2][0].lower() , 'cache-control' )
-        self.assertEqual( headers[2][1], 'max-age=0' )
+        mgr.addPolicy( 'first', 'python:1', 'mtime', 1, 0, 1, 0, 'vary', 'etag', None, 2, 1, 0, 1, 0 )
+        p = mgr._policies['first']
+        self.assertEqual(p.getPolicyId(), 'first')
+        self.assertEqual(p.getPredicate(), 'python:1')
+        self.assertEqual(p.getMTimeFunc(), 'mtime')
+        self.assertEqual(p.getMaxAgeSecs(), 1)
+        self.assertEqual(p.getNoCache(), 0)
+        self.assertEqual(p.getNoStore(), 1)
+        self.assertEqual(p.getMustRevalidate(), 0)
+        self.assertEqual(p.getVary(), 'vary')
+        self.assertEqual(p.getETagFunc(), 'etag')
+        self.assertEqual(p.getSMaxAgeSecs(), 2)
+        self.assertEqual(p.getProxyRevalidate(), 1)
+        self.assertEqual(p.getPublic(), 0)
+        self.assertEqual(p.getPrivate(), 1)
+        self.assertEqual(p.getNoTransform(), 0)
+        
+        mgr.updatePolicy( 'first', 'python:0', 'mtime2', 2, 1, 0, 1, 'vary2', 'etag2', None, 1, 0, 1, 0, 1 )
+        p = mgr._policies['first']
+        self.assertEqual(p.getPolicyId(), 'first')
+        self.assertEqual(p.getPredicate(), 'python:0')
+        self.assertEqual(p.getMTimeFunc(), 'mtime2')
+        self.assertEqual(p.getMaxAgeSecs(), 2)
+        self.assertEqual(p.getNoCache(), 1)
+        self.assertEqual(p.getNoStore(), 0)
+        self.assertEqual(p.getMustRevalidate(), 1)
+        self.assertEqual(p.getVary(), 'vary2')
+        self.assertEqual(p.getETagFunc(), 'etag2')
+        self.assertEqual(p.getSMaxAgeSecs(), 1)
+        self.assertEqual(p.getProxyRevalidate(), 0)
+        self.assertEqual(p.getPublic(), 1)
+        self.assertEqual(p.getPrivate(), 0)
+        self.assertEqual(p.getNoTransform(), 1)
 
     def test_reorder( self ):
 
@@ -358,7 +465,7 @@
     def test_lookupNoMatch( self ):
 
         mgr = self._makeOneWithPolicies()
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
+        headers = mgr.getHTTPCachingHeaders( content=DummyContent2(self._epoch)
                                            , view_method='foo_view'
                                            , keywords={}
                                            , time=self._epoch
@@ -368,7 +475,7 @@
     def test_lookupMatchFoo( self ):
 
         mgr = self._makeOneWithPolicies()
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
+        headers = mgr.getHTTPCachingHeaders( content=DummyContent2(self._epoch)
                                            , view_method='foo_view'
                                            , keywords={ 'foo' : 1 }
                                            , time=self._epoch
@@ -381,7 +488,7 @@
     def test_lookupMatchBar( self ):
 
         mgr = self._makeOneWithPolicies()
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
+        headers = mgr.getHTTPCachingHeaders( content=DummyContent2(self._epoch)
                                            , view_method='foo_view'
                                            , keywords={ 'bar' : 1 }
                                            , time=self._epoch
@@ -399,7 +506,7 @@
     def test_lookupMatchBaz( self ):
 
         mgr = self._makeOneWithPolicies()
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
+        headers = mgr.getHTTPCachingHeaders( content=DummyContent2(self._epoch)
                                            , view_method='foo_view'
                                            , keywords={ 'baz' : 1 }
                                            , time=self._epoch
@@ -420,7 +527,7 @@
     def test_lookupMatchQux( self ):
 
         mgr = self._makeOneWithPolicies()
-        headers = mgr.getHTTPCachingHeaders( content=DummyContent(self._epoch)
+        headers = mgr.getHTTPCachingHeaders( content=DummyContent2(self._epoch)
                                            , view_method='foo_view'
                                            , keywords={ 'qux' : 1 }
                                            , time=self._epoch
@@ -439,11 +546,255 @@
         self.assertEqual( headers[2][1] , 'max-age=86400' )
 
 
+class CachingPolicyManager304Tests(RequestTest, FSDVTest):
+
+    def setUp(self):
+        RequestTest.setUp(self)
+        FSDVTest.setUp(self)
+
+        now = DateTime()
+
+        # Create a fake portal and the tools we need
+        self.portal = DummySite(id='portal').__of__(self.root)
+        self.portal._setObject('portal_types', DummyTool())
+
+        # This is a FSPageTemplate that will be used as the View for 
+        # our content objects. It doesn't matter what it returns.
+        path = os.path.join(self.skin_path_name, 'testPT2.pt')
+        self.portal._setObject('dummy_view', FSPageTemplate('dummy_view', path))
+
+        uf = self.root.acl_users
+        password = 'secret'
+        uf.userFolderAddUser(portal_owner, password, ['Manager'], [])
+        user = uf.getUserById(portal_owner)
+        if not hasattr(user, 'aq_base'):
+            user = user.__of__(uf)
+        newSecurityManager(None, user)
+        owner_auth = '%s:%s' % (portal_owner, password)
+        self.auth_header = "Basic %s" % base64.encodestring(owner_auth)
+        
+        self.portal._setObject('doc1', DummyContent('doc1'))
+        self.portal._setObject('doc2', DummyContent('doc2'))
+        self.portal._setObject('doc3', DummyContent('doc3'))
+        self.portal.doc1.modified_date = now
+        self.portal.doc2.modified_date = now
+        self.portal.doc3.modified_date = now
+
+        CachingPolicyManager.manage_addCachingPolicyManager(self.portal)
+        cpm = self.portal.caching_policy_manager
+
+        # This policy only applies to doc1. It will not emit any ETag header
+        # but it enables If-modified-since handling.
+        cpm.addPolicy(policy_id = 'policy_no_etag',
+                      predicate = 'python:object.getId()=="doc1"',
+                      mtime_func = '',
+                      max_age_secs = 0,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = '',
+                      etag_func = '',
+                      enable_304s = 1)
+
+        # This policy only applies to doc2. It will emit an ETag with 
+        # the constant value "abc" and also enable if-modified-since handling.
+        cpm.addPolicy(policy_id = 'policy_etag',
+                      predicate = 'python:object.getId()=="doc2"',
+                      mtime_func = '',
+                      max_age_secs = 0,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = '',
+                      etag_func = 'string:abc',
+                      enable_304s = 1)
+
+        # This policy only applies to doc3. Etags with constant values of
+        # "abc" are emitted, but if-modified-since handling is turned off.
+        cpm.addPolicy(policy_id = 'policy_disabled',
+                      predicate = 'python:object.getId()=="doc3"',
+                      mtime_func = '',
+                      max_age_secs = 0,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = '',
+                      etag_func = 'string:abc',
+                      enable_304s = 0)
+
+
+    def tearDown(self):
+        RequestTest.tearDown(self)
+        FSDVTest.tearDown(self)
+
+
+    def _cleanup(self):
+        # Clean up request and response
+        req = self.portal.REQUEST
+
+        for header in ( 'IF_MODIFIED_SINCE'
+                      , 'HTTP_AUTHORIZATION'
+                      , 'IF_NONE_MATCH'
+                      ):
+            if req.environ.get(header, None) is not None:
+                del req.environ[header]
+
+        req.RESPONSE.setStatus(200)
+
+
+    def testUnconditionalGET(self):
+        # In this case the Request does not specify any if-modified-since
+        # value to take into account, thereby completely circumventing any
+        # if-modified-since handling. This must not produce a response status
+        # of 304, regardless of any other headers.
+        self.portal.doc1()
+        response = self.portal.REQUEST.RESPONSE
+        self.assertEqual(response.getStatus(), 200)
+
+
+    def testConditionalGETNoETag(self):
+        yesterday = DateTime() - 1
+        doc1 = self.portal.doc1
+        request = doc1.REQUEST
+        response = request.RESPONSE
+
+        # If doc1 has beeen modified since yesterday (which it has), we want
+        # the full rendering.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(yesterday)
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc1()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        # If doc1 has been modified since its creation (which it hasn't), we
+        # want the full rendering. This must return a 304 response.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(doc1.modified_date)
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc1() 
+        self.assertEqual(response.getStatus(), 304)
+        self._cleanup()
+
+        # ETag handling is not enabled in the policy for doc1, so asking for
+        # one will not produce any matches. We get the full rendering.
+        request.environ['IF_NONE_MATCH'] = '"123"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc1()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        # We are asking for an ETag as well as modifications after doc2 has 
+        # been created. Both won't match and wwe get the full rendering.
+        request.environ['IF_NONE_MATCH'] = '"123"'
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(doc1.modified_date)
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc1()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+        
+
+    def testConditionalGETETag(self):
+        yesterday = DateTime() - 1
+        doc2 = self.portal.doc2
+        request = doc2.REQUEST
+        response = request.RESPONSE
+
+        # Has doc2 been modified since yesterday? Yes it has, so we get the
+        # full rendering.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(yesterday)
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        # If doc2 has not been modified since its creation (which it hasn't),
+        # we would get a 304 here. However, the policy for doc2 also expects
+        # to get an ETag in the request, which we are not setting here. So
+        # the policy fails and we get a full rendering.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(doc2.modified_date)
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        # Now we are setting an ETag in our request, but an ETag that does not
+        # match the policy's expected value. The policy fails and we get the
+        # full rendering.
+        request.environ['IF_NONE_MATCH'] = '"123"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        # Here we provide the correct and matching ETag value, and we don't
+        # specify any if-modified-since condition. This is enough for our
+        # policy to trigger 304.
+        request.environ['IF_NONE_MATCH'] = '"abc"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 304)
+        self._cleanup()
+        
+        # We specify an ETag and a modification time condition that dooes not 
+        # match, so we get the full rendering
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(doc2.modified_date)
+        request.environ['IF_NONE_MATCH'] = '"123"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        # We hand in a matching modified time condition which is supposed to
+        # trigger full rendering. This will lead the ETag condition to be
+        # overrridden.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(yesterday)
+        request.environ['IF_NONE_MATCH'] = '"abc"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+        
+        # Now we pass an ETag that matches the policy and a modified time
+        # condition that is not fulfilled. It is safe to serve a 304.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(doc2.modified_date)
+        request.environ['IF_NONE_MATCH'] = '"abc"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc2()
+        self.assertEqual(response.getStatus(), 304)
+        self._cleanup()
+
+        
+    def testConditionalGETDisabled(self):
+        yesterday = DateTime() - 1
+        doc3 = self.portal.doc3
+        request = doc3.REQUEST
+        response = request.RESPONSE
+
+        # Our policy disables any 304-handling, so even though the ETag matches
+        # the policy, we will get the full rendering.
+        request.environ['IF_NONE_MATCH'] = '"abc"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc3()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+        
+        # Now both the ETag and the modified condition would trigger a 304
+        # response *if* 304-handling was enabled. It is not in our policy, so
+        # we get the full rendering again.
+        request.environ['IF_MODIFIED_SINCE'] = rfc1123_date(doc3.modified_date)
+        request.environ['IF_NONE_MATCH'] = '"abc"'
+        request.environ['HTTP_AUTHORIZATION'] = self.auth_header
+        doc3()
+        self.assertEqual(response.getStatus(), 200)
+        self._cleanup()
+
+        
 def test_suite():
     return TestSuite((
         makeSuite(CachingPolicyTests),
         makeSuite(CachingPolicyManagerTests),
+        makeSuite(CachingPolicyManager304Tests),
         ))
 
 if __name__ == '__main__':
     main(defaultTest='test_suite')
+

Modified: CMF/trunk/CMFCore/utils.py
===================================================================
--- CMF/trunk/CMFCore/utils.py	2005-09-13 18:15:07 UTC (rev 38457)
+++ CMF/trunk/CMFCore/utils.py	2005-09-13 18:15:42 UTC (rev 38458)
@@ -31,6 +31,7 @@
 from Acquisition import aq_inner
 from Acquisition import aq_parent
 from Acquisition import Implicit
+from DateTime import DateTime
 from ExtensionClass import Base
 from Globals import HTMLFile
 from Globals import ImageFile
@@ -46,6 +47,7 @@
 from Products.PageTemplates.Expressions import getEngine
 from Products.PageTemplates.Expressions import SecureModuleImporter
 from zope.i18nmessageid import MessageIDFactory
+from thread import allocate_lock
 
 from exceptions import AccessControl_Unauthorized
 from exceptions import NotFound
@@ -268,10 +270,146 @@
             something_changed = 1
     return something_changed
 
+
+# Parse a string of etags from an If-None-Match header
+# Code follows ZPublisher.HTTPRequest.parse_cookie
+parse_etags_lock=allocate_lock()
+def parse_etags( text
+               , result=None
+                # quoted etags (assumed separated by whitespace + a comma)
+               , etagre_quote = re.compile('(\s*\"([^\"]*)\"\s*,{0,1})')
+                # non-quoted etags (assumed separated by whitespace + a comma)
+               , etagre_noquote = re.compile('(\s*([^,]*)\s*,{0,1})')
+               , acquire=parse_etags_lock.acquire
+               , release=parse_etags_lock.release
+               ):
+
+    if result is None: result=[]
+    if not len(text):
+        return result
+
+    acquire()
+    try:
+        m = etagre_quote.match(text)
+        if m:
+            # Match quoted etag (spec-observing client)
+            l     = len(m.group(1))
+            value = m.group(2)
+        else:
+            # Match non-quoted etag (lazy client)
+            m = etagre_noquote.match(text)
+            if m:
+                l     = len(m.group(1))
+                value = m.group(2)
+            else:
+                return result
+    finally: release()
+
+    if value:
+        result.append(value)
+    return apply(parse_etags,(text[l:],result))
+
+
+def _checkConditionalGET(obj, extra_context):
+    """A conditional GET is done using one or both of the request
+       headers:
+
+       If-Modified-Since: Date
+       If-None-Match: list ETags (comma delimited, sometimes quoted)
+
+       If both conditions are present, both must be satisfied.
+       
+       This method checks the caching policy manager to see if
+       a content object's Last-modified date and ETag satisfy
+       the conditional GET headers.
+
+       Returns the tuple (last_modified, etag) if the conditional
+       GET requirements are met and None if not.
+
+       It is possible for one of the tuple elements to be None.
+       For example, if there is no If-None-Match header and
+       the caching policy does not specify an ETag, we will
+       just return (last_modified, None).
+       """
+
+    REQUEST = getattr(obj, 'REQUEST', None)
+    if REQUEST is None:
+        return False
+
+    if_modified_since = REQUEST.get_header('If-Modified-Since', None)
+    if_none_match = REQUEST.get_header('If-None-Match', None)
+
+    if if_modified_since is None and if_none_match is None:
+        # not a conditional GET
+        return False
+
+    manager = getToolByName(obj, 'caching_policy_manager', None)
+    ret = manager.getModTimeAndETag(aq_parent(obj), obj.getId(), extra_context)
+    if ret is None:
+        # no appropriate policy or 304s not enabled
+        return  False 
+
+    (content_mod_time, content_etag) = ret
+    if content_mod_time:
+        mod_time_secs = content_mod_time.timeTime()
+    else:
+        mod_time_secs = None
+    
+    if if_modified_since:
+        # from CMFCore/FSFile.py:
+        if_modified_since = if_modified_since.split(';')[0]
+        # Some proxies seem to send invalid date strings for this
+        # header. If the date string is not valid, we ignore it
+        # rather than raise an error to be generally consistent
+        # with common servers such as Apache (which can usually
+        # understand the screwy date string as a lucky side effect
+        # of the way they parse it).
+        try:
+            if_modified_since=long(DateTime(if_modified_since).timeTime())
+        except:
+            if_mod_since=None
+                
+    client_etags = None
+    if if_none_match:
+        client_etags = parse_etags(if_none_match)
+
+    if not if_modified_since and not client_etags:
+        # not a conditional GET, or headers are messed up
+        return False
+
+    if if_modified_since:
+        if ( not content_mod_time or 
+             mod_time_secs < 0 or 
+             mod_time_secs > if_modified_since ):
+            return False
+        
+    if client_etags:
+        if ( not content_etag or 
+             (content_etag not in client_etags and '*' not in client_etags) ):
+            return False
+    else:
+        # If we generate an ETag, don't validate the conditional GET unless 
+        # the client supplies an ETag
+        # This may be more conservative than the spec requires, but we are 
+        # already _way_ more conservative.
+        if content_etag:
+            return False
+
+    response = REQUEST.RESPONSE
+    if content_mod_time:
+        response.setHeader('Last-modified', str(content_mod_time))
+    if content_etag:
+        response.setHeader('ETag', content_etag, literal=1)
+    response.setStatus(304)
+            
+    return True
+    
+
 security.declarePrivate('_setCacheHeaders')
 def _setCacheHeaders(obj, extra_context):
     """Set cache headers according to cache policy manager for the obj."""
     REQUEST = getattr(obj, 'REQUEST', None)
+
     if REQUEST is not None:
         content = aq_parent(obj)
         manager = getToolByName(obj, 'caching_policy_manager', None)



More information about the CMF-checkins mailing list