[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