[CMF-checkins] SVN: CMF/branches/1.4/CMFCore/ - Merge -r 38412:38413 from sidnei-cachingpolicymanager-backport-branch. Backported caching policy manager improvements by Geoff Davis

Sidnei da Silva sidnei at enfoldsystems.com
Thu Sep 8 22:42:45 EDT 2005


Log message for revision 38414:
  
  - Merge -r 38412:38413 from sidnei-cachingpolicymanager-backport-branch. Backported caching policy manager improvements by Geoff Davis
  

Changed:
  U   CMF/branches/1.4/CMFCore/CachingPolicyManager.py
  U   CMF/branches/1.4/CMFCore/FSPageTemplate.py
  U   CMF/branches/1.4/CMFCore/dtml/cachingPolicies.dtml
  _U  CMF/branches/1.4/CMFCore/tests/fake_skins/fake_skin/testDTML.dtml
  _U  CMF/branches/1.4/CMFCore/tests/fake_skins/fake_skin/testDTML.dtml.metadata
  A   CMF/branches/1.4/CMFCore/tests/framework.py
  U   CMF/branches/1.4/CMFCore/tests/test_CachingPolicyManager.py
  A   CMF/branches/1.4/CMFCore/tests/test_Template304Handling.py
  U   CMF/branches/1.4/CMFCore/utils.py

-=-
Modified: CMF/branches/1.4/CMFCore/CachingPolicyManager.py
===================================================================
--- CMF/branches/1.4/CMFCore/CachingPolicyManager.py	2005-09-09 02:33:38 UTC (rev 38413)
+++ CMF/branches/1.4/CMFCore/CachingPolicyManager.py	2005-09-09 02:42:44 UTC (rev 38414)
@@ -57,7 +57,10 @@
     if time is None:
         time = DateTime()
 
+    # The name "content" is deprecated and will go away in CMF 1.7,
+    # please use "object" in your policy
     data = { 'content'  : content
+           , 'object'   : content
            , 'view'     : view_method
            , 'keywords' : keywords
            , 'request'  : getattr( content, 'REQUEST', {} )
@@ -99,6 +102,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
@@ -108,6 +115,10 @@
             Cookie headers into account when deciding what cached object to 
             choose and serve in response to a request.
 
+          - The "ETag" HTTP response header will be set if a value is
+            provided. The value is a TALES expression and the result 
+            after evaluation will be used as the ETag header value.
+
           - Other tokens will be added to the "Cache-control" HTTP response
             header as follows:
 
@@ -116,6 +127,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
@@ -127,6 +146,13 @@
                 , no_store=0
                 , 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:
@@ -138,14 +164,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 ):
         """
@@ -167,6 +203,11 @@
         """
         return self._max_age_secs
 
+    def getSMaxAgeSecs( self ):
+        """
+        """
+        return getattr(self, '_s_max_age_secs', None)
+
     def getNoCache( self ):
         """
         """
@@ -182,11 +223,51 @@
         """
         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 ):
         """
         """
         return getattr(self, '_vary', '')
 
+    def getETagFunc( self ):
+        """
+        """
+        etag_func_text = ''
+        etag_func = getattr(self, '_etag_func', None)
+
+        if etag_func is not None:
+            etag_func_text = etag_func.text
+
+        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
@@ -195,7 +276,7 @@
         """
         headers = []
 
-        if self._predicate( expr_context ):
+        if self.testPredicate( expr_context ):
 
             mtime = self._mtime_func( expr_context )
 
@@ -209,30 +290,50 @@
 
             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' )
+                headers.append(('Pragma', 'no-cache')) # tell HTTP 1.0 clients not to 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 ) ) )
 
             if self.getVary():
                 headers.append( ( 'Vary', self._vary ) )
 
+            if self.getETagFunc():
+                headers.append( ( 'ETag', self._etag_func( expr_context ) ) )
+
         return headers
 
 
+
 class CachingPolicyManager( SimpleItem ):
     """
         Manage the set of CachingPolicy objects for the site;  dispatch
@@ -279,18 +380,30 @@
     security.declareProtected( ManagePortal, 'addPolicy' )
     def addPolicy( self
                  , policy_id
-                 , predicate        # TALES expr (def. 'python:1')
-                 , mtime_func       # TALES expr (def. 'content/modified')
-                 , max_age_secs     # integer, seconds (def. 0)
-                 , no_cache         # boolean (def. 0)
-                 , no_store         # boolean (def. 0)
-                 , must_revalidate  # boolean (def. 0)
-                 , vary
+                 , 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
@@ -299,6 +412,13 @@
                        , no_store
                        , 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()
@@ -310,18 +430,30 @@
     security.declareProtected( ManagePortal, 'updatePolicy' )
     def updatePolicy( self
                     , policy_id
-                    , predicate         # TALES expr (def. 'python:1')
-                    , mtime_func        # TALES expr (def. 'content/modified')
-                    , max_age_secs      # integer, seconds
-                    , no_cache          # boolean (def. 0)
-                    , no_store          # boolean (def. 0)
-                    , must_revalidate   # boolean (def. 0)
-                    , vary
+                    , 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
@@ -330,6 +462,13 @@
                           , no_store
                           , 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()
@@ -399,6 +538,13 @@
                   , no_store
                   , 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.
@@ -419,6 +565,13 @@
                                                    , no_store
                                                    , 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 )
@@ -434,6 +587,13 @@
                      , no_store
                      , 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.
@@ -449,6 +609,13 @@
                                                    , no_store
                                                    , must_revalidate
                                                    , vary
+                                                   , etag_func
+                                                   , s_max_age_secs
+                                                   , proxy_revalidate
+                                                   , public
+                                                   , private
+                                                   , no_transform
+                                                   , enable_304s
                                                    )
 
     security.declarePrivate( '_reorderPolicy' )
@@ -501,7 +668,30 @@
 
         return ()
 
+    # 304 handling helper
+    security.declareProtected( View, 'getHTTPCachingHeaders' )
+    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 )
 
 def manage_addCachingPolicyManager( self, REQUEST=None ):

Modified: CMF/branches/1.4/CMFCore/FSPageTemplate.py
===================================================================
--- CMF/branches/1.4/CMFCore/FSPageTemplate.py	2005-09-09 02:33:38 UTC (rev 38413)
+++ CMF/branches/1.4/CMFCore/FSPageTemplate.py	2005-09-09 02:42:44 UTC (rev 38414)
@@ -33,7 +33,7 @@
 from CMFCorePermissions import View
 from CMFCorePermissions import FTPAccess
 from FSObject import FSObject
-from utils import _setCacheHeaders
+from utils import _setCacheHeaders, _checkConditionalGET
 
 xml_detect_re = re.compile('^\s*<\?xml\s+')
 
@@ -120,6 +120,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/branches/1.4/CMFCore/dtml/cachingPolicies.dtml
===================================================================
--- CMF/branches/1.4/CMFCore/dtml/cachingPolicies.dtml	2005-09-09 02:33:38 UTC (rev 38413)
+++ CMF/branches/1.4/CMFCore/dtml/cachingPolicies.dtml	2005-09-09 02:42:44 UTC (rev 38414)
@@ -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,16 +93,65 @@
        </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>
+         <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">
          <input type="submit" name="updatePolicy:method" value=" Change ">
@@ -128,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>
 
@@ -176,13 +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>
+         <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 ">


Property changes on: CMF/branches/1.4/CMFCore/tests/fake_skins/fake_skin/testDTML.dtml
___________________________________________________________________
Name: svn:keywords
   + Author Date Id Revision


Property changes on: CMF/branches/1.4/CMFCore/tests/fake_skins/fake_skin/testDTML.dtml.metadata
___________________________________________________________________
Name: svn:keywords
   + Author Date Id Revision

Copied: CMF/branches/1.4/CMFCore/tests/framework.py (from rev 38413, CMF/branches/sidnei-cachingpolicymanager-backport-branch/CMFCore/tests/framework.py)


Property changes on: CMF/branches/1.4/CMFCore/tests/framework.py
___________________________________________________________________
Name: svn:keywords
   + Author Date Id Revision
Name: svn:eol-style
   + native

Modified: CMF/branches/1.4/CMFCore/tests/test_CachingPolicyManager.py
===================================================================
--- CMF/branches/1.4/CMFCore/tests/test_CachingPolicyManager.py	2005-09-09 02:33:38 UTC (rev 38413)
+++ CMF/branches/1.4/CMFCore/tests/test_CachingPolicyManager.py	2005-09-09 02:42:44 UTC (rev 38414)
@@ -160,19 +160,35 @@
         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 ):
 
         policy = self._makePolicy( 'noStore', no_store=1 )
@@ -198,10 +214,10 @@
                         , rfc1123_date(self._epoch.timeTime()) )
         self.assertEqual( headers[1][0].lower() , 'cache-control' )
         self.assertEqual( headers[1][1] , 'must-revalidate' )
-        
-    def test_combined( self ):
 
-        policy = self._makePolicy( 'noStore', no_cache=1, no_store=1 )
+    def test_proxyRevalidate( self ):
+
+        policy = self._makePolicy( 'proxyRevalidate', proxy_revalidate=1 )
         context = self._makeContext()
         headers = policy.getHeaders( context )
 
@@ -210,9 +226,90 @@
         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][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
+        policy = self._makePolicy( 'ETag', etag_func='' )
+        context = self._makeContext()
+        headers = policy.getHeaders( context )
+
+        self.assertEqual( len( headers ), 1)
+        self.assertEqual( headers[0][0].lower() , 'last-modified' )
+        self.assertEqual( headers[0][1]
+                        , rfc1123_date(self._epoch.timeTime()) )
+
+        policy = self._makePolicy( 'ETag', etag_func='string:foo' )
+        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(), 'etag' )
+        self.assertEqual( headers[1][1], 'foo' )
+
+    def test_combined( self ):
+
+        policy = self._makePolicy( 'noStore', no_cache=1, no_store=1 )
+        context = self._makeContext()
+        headers = policy.getHeaders( context )
+
+        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() , '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( unittest.TestCase ):
 
     def setUp(self):
@@ -251,28 +348,47 @@
         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 ):
 
@@ -283,7 +399,7 @@
         for policy_id in policy_ids:
             mgr._addPolicy( policy_id
                           , 'python:"%s" in keywords.keys()' % policy_id
-                          , None, 0, 0, 0, 0, '' )
+                          , None, 0, 0, 0, 0, '', '')
 
         ids = tuple( map( lambda x: x[0], mgr.listPolicies() ) )
         self.assertEqual( ids, policy_ids )
@@ -306,7 +422,7 @@
         for policy_id, max_age_secs in policy_tuples:
             mgr._addPolicy( policy_id
                           , 'python:"%s" in keywords.keys()' % policy_id
-                          , None, max_age_secs, 0, 0, 0, '' )
+                          , None, max_age_secs, 0, 0, 0, '', '' )
 
         return mgr
 

Copied: CMF/branches/1.4/CMFCore/tests/test_Template304Handling.py (from rev 38413, CMF/branches/sidnei-cachingpolicymanager-backport-branch/CMFCore/tests/test_Template304Handling.py)


Property changes on: CMF/branches/1.4/CMFCore/tests/test_Template304Handling.py
___________________________________________________________________
Name: svn:keywords
   + Author Date Id Revision
Name: svn:eol-style
   + native

Modified: CMF/branches/1.4/CMFCore/utils.py
===================================================================
--- CMF/branches/1.4/CMFCore/utils.py	2005-09-09 02:33:38 UTC (rev 38413)
+++ CMF/branches/1.4/CMFCore/utils.py	2005-09-09 02:42:44 UTC (rev 38414)
@@ -18,23 +18,26 @@
 from types import StringType, UnicodeType
 from copy import deepcopy
 
-from Globals import package_home
-from Globals import HTMLFile
-from Globals import ImageFile
-from Globals import InitializeClass
-from Globals import MessageDialog
-
-from ExtensionClass import Base
-from Acquisition import Implicit
-from Acquisition import aq_base, aq_get, aq_inner, aq_parent
-
 from AccessControl import ClassSecurityInfo
 from AccessControl import ModuleSecurityInfo
 from AccessControl import getSecurityManager
 from AccessControl.Permission import Permission
 from AccessControl.PermissionRole import rolesForPermissionOn
 from AccessControl.Role import gather_permissions
-
+from Acquisition import aq_base
+from Acquisition import aq_get
+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
+from Globals import InitializeClass
+from Globals import MessageDialog
+from Globals import package_home
+from OFS.misc_ import misc_ as misc_images
+from OFS.misc_ import Misc_ as MiscImage
 from OFS.PropertyManager import PropertyManager
 from OFS.SimpleItem import SimpleItem
 from OFS.PropertySheets import PropertySheets
@@ -46,6 +49,7 @@
     UNIQUE = 2
 from Products.PageTemplates.Expressions import getEngine
 from Products.PageTemplates.Expressions import SecureModuleImporter
+from thread import allocate_lock
 
 
 security = ModuleSecurityInfo( 'Products.CMFCore.utils' )
@@ -307,10 +311,139 @@
             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,
+                etagre_quote = re.compile('(\s*\"([^\"]*)\"\s*,{0,1})'), # quoted etags (assumed separated by whitespace + a comma)
+                etagre_noquote = re.compile('(\s*([^,]*)\s*,{0,1})'),    # non-quoted etags (assumed separated by whitespace + a comma)
+                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