vendor/CMF/1.6.3/CMFCore

view CachingPolicyManager.py @ 0:587011552858

import CMF 1.6.3
author bdelbosc
date Mon, 23 Apr 2007 13:58:01 +0000
parents
children
line source
1 ##############################################################################
2 #
3 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
4 #
5 # This software is subject to the provisions of the Zope Public License,
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10 # FOR A PARTICULAR PURPOSE.
11 #
12 ##############################################################################
13 """Caching tool implementation.
15 $Id$
16 """
18 from AccessControl import ClassSecurityInfo
19 from App.Common import rfc1123_date
20 from DateTime.DateTime import DateTime
21 from Globals import DTMLFile
22 from Globals import InitializeClass
23 from Globals import PersistentMapping
24 from OFS.SimpleItem import SimpleItem
25 from Products.PageTemplates.Expressions import getEngine
26 from Products.PageTemplates.Expressions import SecureModuleImporter
28 from permissions import ManagePortal
29 from permissions import View
30 from Expression import Expression
31 from interfaces.CachingPolicyManager \
32 import CachingPolicyManager as ICachingPolicyManager
33 from utils import _dtmldir
34 from utils import getToolByName
36 from zope.interface import implements
37 from interfaces import ICachingPolicy
39 def createCPContext( content, view_method, keywords, time=None ):
40 """
41 Construct an expression context for TALES expressions,
42 for use by CachingPolicy objects.
43 """
44 pm = getToolByName( content, 'portal_membership', None )
45 if not pm or pm.isAnonymousUser():
46 member = None
47 else:
48 member = pm.getAuthenticatedMember()
50 if time is None:
51 time = DateTime()
53 # The name "content" is deprecated and will go away in CMF 2.0,
54 # please use "object" in your policy
55 data = { 'content' : content
56 , 'object' : content
57 , 'view' : view_method
58 , 'keywords' : keywords
59 , 'request' : getattr( content, 'REQUEST', {} )
60 , 'member' : member
61 , 'modules' : SecureModuleImporter
62 , 'nothing' : None
63 , 'time' : time
64 }
66 return getEngine().getContext( data )
69 class CachingPolicy:
70 """
71 Represent a single class of cachable objects:
73 - class membership is defined by 'predicate', a TALES expression
74 with access to the following top-level names:
76 'object' -- the object itself
78 'view' -- the name of the view method
80 'keywords' -- keywords passed to the request
82 'request' -- the REQUEST object itself
84 'member' -- the authenticated member, or None if anonymous
86 'modules' -- usual TALES access-with-import
88 'nothing' -- None
90 'time' -- A DateTime object for the current date and time
92 - mtime_func is used to set the "Last-modified" HTTP response
93 header, which is another TALES expression evaluated
94 against the same namespace. If not specified explicitly,
95 uses 'object/modified'. mtime_func is also used in responding
96 to conditional GETs.
98 - The "Expires" HTTP response header and the "max-age" token of
99 the "Cache-control" header will be set using 'max_age_secs',
100 if passed; it should be an integer value in seconds.
102 - The "s-maxage" token of the "Cache-control" header will be
103 set using 's_max_age_secs', if passed; it should be an integer
104 value in seconds.
106 - The "Vary" HTTP response headers will be set if a value is
107 provided. The Vary header is described in RFC 2616. In essence,
108 it instructs caches that respect this header (such as Squid
109 after version 2.4) to distinguish between requests not just by
110 the request URL, but also by values found in the headers showing
111 in the Vary tag. "Vary: Cookie" would force Squid to also take
112 Cookie headers into account when deciding what cached object to
113 choose and serve in response to a request.
115 - The "ETag" HTTP response header will be set if a value is
116 provided. The value is a TALES expression and the result
117 after evaluation will be used as the ETag header value.
119 - Other tokens will be added to the "Cache-control" HTTP response
120 header as follows:
122 'no_cache=1' argument => "no-cache" token
124 'no_store=1' argument => "no-store" token
126 'must_revalidate=1' argument => "must-revalidate" token
128 'proxy_revalidate=1' argument => "proxy-revalidate" token
130 'public=1' argument => "public" token
132 'private=1' argument => "private" token
134 'no_transform=1' argument => "no-transform" token
136 - The last_modified argument is used to determine whether to add a
137 Last-Modified header. last_modified=1 by default. There appears
138 to be a bug in IE 6 (and possibly other versions) that uses the
139 Last-Modified header plus some heuristics rather than the other
140 explicit caching headers to determine whether to render content
141 from the cache. If you set, say, max-age=0, must-revalidate and
142 have a Last-Modified header some time in the past, IE will
143 recognize that the page in cache is stale and will request an
144 update from the server BUT if you have a Last-Modified header
145 with an older date, will then ignore the update and render from
146 the cache, so you may want to disable the Last-Modified header
147 when controlling caching using Cache-Control headers.
149 - The pre-check and post-check Cache-Control tokens are Microsoft
150 proprietary tokens added to IE 5+. Documentation can be found
151 here: http://msdn.microsoft.com/workshop/author/perf/perftips.asp
152 Unfortunately these are needed to make IE behave correctly.
154 """
156 implements(ICachingPolicy)
158 def __init__( self
159 , policy_id
160 , predicate=''
161 , mtime_func=''
162 , max_age_secs=None
163 , no_cache=0
164 , no_store=0
165 , must_revalidate=0
166 , vary=''
167 , etag_func=''
168 , s_max_age_secs=None
169 , proxy_revalidate=0
170 , public=0
171 , private=0
172 , no_transform=0
173 , enable_304s=0
174 , last_modified=1
175 , pre_check=None
176 , post_check=None
177 ):
179 if not predicate:
180 predicate = 'python:1'
182 if not mtime_func:
183 mtime_func = 'object/modified'
185 if max_age_secs is not None:
186 if str(max_age_secs).strip() == '':
187 max_age_secs = None
188 else:
189 max_age_secs = int( max_age_secs )
191 if s_max_age_secs is not None:
192 if str(s_max_age_secs).strip() == '':
193 s_max_age_secs = None
194 else:
195 s_max_age_secs = int( s_max_age_secs )
197 if pre_check is not None:
198 if str(pre_check).strip() == '':
199 pre_check = None
200 else:
201 pre_check = int(pre_check)
203 if post_check is not None:
204 if str(post_check).strip() == '':
205 post_check = None
206 else:
207 post_check = int(post_check)
209 self._policy_id = policy_id
210 self._predicate = Expression( text=predicate )
211 self._mtime_func = Expression( text=mtime_func )
212 self._max_age_secs = max_age_secs
213 self._s_max_age_secs = s_max_age_secs
214 self._no_cache = int( no_cache )
215 self._no_store = int( no_store )
216 self._must_revalidate = int( must_revalidate )
217 self._proxy_revalidate = int( proxy_revalidate )
218 self._public = int( public )
219 self._private = int( private )
220 self._no_transform = int( no_transform )
221 self._vary = vary
222 self._etag_func = Expression( text=etag_func )
223 self._enable_304s = int ( enable_304s )
224 self._last_modified = int( last_modified )
225 self._pre_check = pre_check
226 self._post_check = post_check
228 def getPolicyId( self ):
229 """
230 """
231 return self._policy_id
233 def getPredicate( self ):
234 """
235 """
236 return self._predicate.text
238 def getMTimeFunc( self ):
239 """
240 """
241 return self._mtime_func.text
243 def getMaxAgeSecs( self ):
244 """
245 """
246 return self._max_age_secs
248 def getSMaxAgeSecs( self ):
249 """
250 """
251 return getattr(self, '_s_max_age_secs', None)
253 def getNoCache( self ):
254 """
255 """
256 return self._no_cache
258 def getNoStore( self ):
259 """
260 """
261 return self._no_store
263 def getMustRevalidate( self ):
264 """
265 """
266 return self._must_revalidate
268 def getProxyRevalidate( self ):
269 """
270 """
271 return getattr(self, '_proxy_revalidate', 0)
273 def getPublic( self ):
274 """
275 """
276 return getattr(self, '_public', 0)
278 def getPrivate( self ):
279 """
280 """
281 return getattr(self, '_private', 0)
283 def getNoTransform( self ):
284 """
285 """
286 return getattr(self, '_no_transform', 0)
288 def getVary( self ):
289 """
290 """
291 return getattr(self, '_vary', '')
293 def getETagFunc( self ):
294 """
295 """
296 etag_func_text = ''
297 etag_func = getattr(self, '_etag_func', None)
299 if etag_func is not None:
300 etag_func_text = etag_func.text
302 return etag_func_text
304 def getEnable304s(self):
305 """
306 """
307 return getattr(self, '_enable_304s', 0)
309 def getLastModified(self):
310 """Should we set the last modified header?"""
311 return getattr(self, '_last_modified', 1)
313 def getPreCheck(self):
314 """
315 """
316 return getattr(self, '_pre_check', None)
318 def getPostCheck(self):
319 """
320 """
321 return getattr(self, '_post_check', None)
323 def testPredicate(self, expr_context):
324 """ Does this request match our predicate?"""
325 return self._predicate(expr_context)
327 def getHeaders( self, expr_context ):
328 """
329 Does this request match our predicate? If so, return a
330 sequence of caching headers as ( key, value ) tuples.
331 Otherwise, return an empty sequence.
332 """
333 headers = []
335 if self.testPredicate( expr_context ):
337 if self.getLastModified():
338 mtime = self._mtime_func( expr_context )
339 if type( mtime ) is type( '' ):
340 mtime = DateTime( mtime )
341 if mtime is not None:
342 mtime_str = rfc1123_date(mtime.timeTime())
343 headers.append( ( 'Last-modified', mtime_str ) )
345 control = []
347 if self.getMaxAgeSecs() is not None:
348 now = expr_context.vars[ 'time' ]
349 exp_time_str = rfc1123_date(now.timeTime() + self._max_age_secs)
350 headers.append( ( 'Expires', exp_time_str ) )
351 control.append( 'max-age=%d' % self._max_age_secs )
353 if self.getSMaxAgeSecs() is not None:
354 control.append( 's-maxage=%d' % self._s_max_age_secs )
356 if self.getNoCache():
357 control.append( 'no-cache' )
358 # The following is for HTTP 1.0 clients
359 headers.append(('Pragma', 'no-cache'))
361 if self.getNoStore():
362 control.append( 'no-store' )
364 if self.getPublic():
365 control.append( 'public' )
367 if self.getPrivate():
368 control.append( 'private' )
370 if self.getMustRevalidate():
371 control.append( 'must-revalidate' )
373 if self.getProxyRevalidate():
374 control.append( 'proxy-revalidate' )
376 if self.getNoTransform():
377 control.append( 'no-transform' )
379 pre_check = self.getPreCheck()
380 if pre_check is not None:
381 control.append('pre-check=%d' % pre_check)
383 post_check = self.getPostCheck()
384 if post_check is not None:
385 control.append('post-check=%d' % post_check)
387 if control:
388 headers.append( ( 'Cache-control', ', '.join( control ) ) )
390 if self.getVary():
391 headers.append( ( 'Vary', self._vary ) )
393 if self.getETagFunc():
394 headers.append( ( 'ETag', self._etag_func( expr_context ) ) )
396 return headers
400 class CachingPolicyManager( SimpleItem ):
401 """
402 Manage the set of CachingPolicy objects for the site; dispatch
403 to them from skin methods.
404 """
406 __implements__ = ICachingPolicyManager
408 id = 'caching_policy_manager'
409 meta_type = 'CMF Caching Policy Manager'
411 security = ClassSecurityInfo()
413 def __init__( self ):
414 self._policy_ids = ()
415 self._policies = PersistentMapping()
417 #
418 # ZMI
419 #
420 manage_options = ( ( { 'label' : 'Policies'
421 , 'action' : 'manage_cachingPolicies'
422 , 'help' : ('CMFCore', 'CPMPolicies.stx')
423 }
424 ,
425 )
426 + SimpleItem.manage_options
427 )
429 security.declareProtected( ManagePortal, 'manage_cachingPolicies' )
430 manage_cachingPolicies = DTMLFile( 'cachingPolicies', _dtmldir )
432 security.declarePublic( 'listPolicies' )
433 def listPolicies( self ):
434 """
435 Return a sequence of tuples,
436 '( policy_id, ( policy, typeObjectName ) )'
437 for all policies in the registry
438 """
439 result = []
440 for policy_id in self._policy_ids:
441 result.append( ( policy_id, self._policies[ policy_id ] ) )
442 return tuple( result )
444 security.declareProtected( ManagePortal, 'addPolicy' )
445 def addPolicy( self
446 , policy_id
447 , predicate # TALES expr (def. 'python:1')
448 , mtime_func # TALES expr (def. 'object/modified')
449 , max_age_secs # integer, seconds (def. 0)
450 , no_cache # boolean (def. 0)
451 , no_store # boolean (def. 0)
452 , must_revalidate # boolean (def. 0)
453 , vary # string value
454 , etag_func # TALES expr (def. '')
455 , REQUEST=None
456 , s_max_age_secs=None # integer, seconds (def. None)
457 , proxy_revalidate=0 # boolean (def. 0)
458 , public=0 # boolean (def. 0)
459 , private=0 # boolean (def. 0)
460 , no_transform=0 # boolean (def. 0)
461 , enable_304s=0 # boolean (def. 0)
462 , last_modified=1 # boolean (def. 1)
463 , pre_check=None # integer, default None
464 , post_check=None # integer, default None
465 ):
466 """
467 Add a caching policy.
468 """
469 if max_age_secs is None or str(max_age_secs).strip() == '':
470 max_age_secs = None
471 else:
472 max_age_secs = int(max_age_secs)
474 if s_max_age_secs is None or str(s_max_age_secs).strip() == '':
475 s_max_age_secs = None
476 else:
477 s_max_age_secs = int(s_max_age_secs)
479 if pre_check is None or str(pre_check).strip() == '':
480 pre_check = None
481 else:
482 pre_check = int(pre_check)
484 if post_check is None or str(post_check).strip() == '':
485 post_check = None
486 else:
487 post_check = int(post_check)
489 self._addPolicy( policy_id
490 , predicate
491 , mtime_func
492 , max_age_secs
493 , no_cache
494 , no_store
495 , must_revalidate
496 , vary
497 , etag_func
498 , s_max_age_secs
499 , proxy_revalidate
500 , public
501 , private
502 , no_transform
503 , enable_304s
504 , last_modified
505 , pre_check
506 , post_check
507 )
508 if REQUEST is not None:
509 REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
510 + '/manage_cachingPolicies'
511 + '?manage_tabs_message='
512 + 'Policy+added.'
513 )
515 security.declareProtected( ManagePortal, 'updatePolicy' )
516 def updatePolicy( self
517 , policy_id
518 , predicate # TALES expr (def. 'python:1')
519 , mtime_func # TALES expr (def. 'object/modified')
520 , max_age_secs # integer, seconds (def. 0)
521 , no_cache # boolean (def. 0)
522 , no_store # boolean (def. 0)
523 , must_revalidate # boolean (def. 0)
524 , vary # string value
525 , etag_func # TALES expr (def. '')
526 , REQUEST=None
527 , s_max_age_secs=None # integer, seconds (def. 0)
528 , proxy_revalidate=0 # boolean (def. 0)
529 , public=0 # boolean (def. 0)
530 , private=0 # boolean (def. 0)
531 , no_transform=0 # boolean (def. 0)
532 , enable_304s=0 # boolean (def. 0)
533 , last_modified=1 # boolean (def. 1)
534 , pre_check=0 # integer, default=None
535 , post_check=0 # integer, default=None
536 ):
537 """
538 Update a caching policy.
539 """
540 if max_age_secs is None or str(max_age_secs).strip() == '':
541 max_age_secs = None
542 else:
543 max_age_secs = int(max_age_secs)
545 if s_max_age_secs is None or str(s_max_age_secs).strip() == '':
546 s_max_age_secs = None
547 else:
548 s_max_age_secs = int(s_max_age_secs)
550 if pre_check is None or str(pre_check).strip() == '':
551 pre_check = None
552 else:
553 pre_check = int(pre_check)
555 if post_check is None or str(post_check).strip() == '':
556 post_check = None
557 else:
558 post_check = int(post_check)
560 self._updatePolicy( policy_id
561 , predicate
562 , mtime_func
563 , max_age_secs
564 , no_cache
565 , no_store
566 , must_revalidate
567 , vary
568 , etag_func
569 , s_max_age_secs
570 , proxy_revalidate
571 , public
572 , private
573 , no_transform
574 , enable_304s
575 , last_modified
576 , pre_check
577 , post_check
578 )
579 if REQUEST is not None:
580 REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
581 + '/manage_cachingPolicies'
582 + '?manage_tabs_message='
583 + 'Policy+updated.'
584 )
586 security.declareProtected( ManagePortal, 'movePolicyUp' )
587 def movePolicyUp( self, policy_id, REQUEST=None ):
588 """
589 Move a caching policy up in the list.
590 """
591 policy_ids = list( self._policy_ids )
592 ndx = policy_ids.index( policy_id )
593 if ndx == 0:
594 msg = "Policy+already+first."
595 else:
596 self._reorderPolicy( policy_id, ndx - 1 )
597 msg = "Policy+moved."
598 if REQUEST is not None:
599 REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
600 + '/manage_cachingPolicies'
601 + '?manage_tabs_message=%s' % msg
602 )
604 security.declareProtected( ManagePortal, 'movePolicyDown' )
605 def movePolicyDown( self, policy_id, REQUEST=None ):
606 """
607 Move a caching policy down in the list.
608 """
609 policy_ids = list( self._policy_ids )
610 ndx = policy_ids.index( policy_id )
611 if ndx == len( policy_ids ) - 1:
612 msg = "Policy+already+last."
613 else:
614 self._reorderPolicy( policy_id, ndx + 1 )
615 msg = "Policy+moved."
616 if REQUEST is not None:
617 REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
618 + '/manage_cachingPolicies'
619 + '?manage_tabs_message=%s' % msg
620 )
622 security.declareProtected( ManagePortal, 'removePolicy' )
623 def removePolicy( self, policy_id, REQUEST=None ):
624 """
625 Remove a caching policy.
626 """
627 self._removePolicy( policy_id )
628 if REQUEST is not None:
629 REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
630 + '/manage_cachingPolicies'
631 + '?manage_tabs_message=Policy+removed.'
632 )
634 #
635 # Policy manipulation methods.
636 #
637 security.declarePrivate( '_addPolicy' )
638 def _addPolicy( self
639 , policy_id
640 , predicate
641 , mtime_func
642 , max_age_secs
643 , no_cache
644 , no_store
645 , must_revalidate
646 , vary
647 , etag_func
648 , s_max_age_secs=None
649 , proxy_revalidate=0
650 , public=0
651 , private=0
652 , no_transform=0
653 , enable_304s=0
654 , last_modified=1
655 , pre_check=None
656 , post_check=None
657 ):
658 """
659 Add a policy to our registry.
660 """
661 policy_id = str( policy_id ).strip()
663 if not policy_id:
664 raise ValueError, "Policy ID is required!"
666 if policy_id in self._policy_ids:
667 raise KeyError, "Policy %s already exists!" % policy_id
669 self._policies[ policy_id ] = CachingPolicy( policy_id
670 , predicate
671 , mtime_func
672 , max_age_secs
673 , no_cache
674 , no_store
675 , must_revalidate
676 , vary
677 , etag_func
678 , s_max_age_secs
679 , proxy_revalidate
680 , public
681 , private
682 , no_transform
683 , enable_304s
684 , last_modified
685 , pre_check
686 , post_check
687 )
688 idlist = list( self._policy_ids )
689 idlist.append( policy_id )
690 self._policy_ids = tuple( idlist )
692 security.declarePrivate( '_updatePolicy' )
693 def _updatePolicy( self
694 , policy_id
695 , predicate
696 , mtime_func
697 , max_age_secs
698 , no_cache
699 , no_store
700 , must_revalidate
701 , vary
702 , etag_func
703 , s_max_age_secs=None
704 , proxy_revalidate=0
705 , public=0
706 , private=0
707 , no_transform=0
708 , enable_304s=0
709 , last_modified=1
710 , pre_check=None
711 , post_check=None
712 ):
713 """
714 Update a policy in our registry.
715 """
716 if policy_id not in self._policy_ids:
717 raise KeyError, "Policy %s does not exist!" % policy_id
719 self._policies[ policy_id ] = CachingPolicy( policy_id
720 , predicate
721 , mtime_func
722 , max_age_secs
723 , no_cache
724 , no_store
725 , must_revalidate
726 , vary
727 , etag_func
728 , s_max_age_secs
729 , proxy_revalidate
730 , public
731 , private
732 , no_transform
733 , enable_304s
734 , last_modified
735 , pre_check
736 , post_check
737 )
739 security.declarePrivate( '_reorderPolicy' )
740 def _reorderPolicy( self, policy_id, newIndex ):
741 """
742 Reorder a policy in our registry.
743 """
744 if policy_id not in self._policy_ids:
745 raise KeyError, "Policy %s does not exist!" % policy_id
747 idlist = list( self._policy_ids )
748 ndx = idlist.index( policy_id )
749 pred = idlist[ ndx ]
750 idlist = idlist[ :ndx ] + idlist[ ndx+1: ]
751 idlist.insert( newIndex, pred )
752 self._policy_ids = tuple( idlist )
754 security.declarePrivate( '_removePolicy' )
755 def _removePolicy( self, policy_id ):
756 """
757 Remove a policy from our registry.
758 """
759 if policy_id not in self._policy_ids:
760 raise KeyError, "Policy %s does not exist!" % policy_id
762 del self._policies[ policy_id ]
763 idlist = list( self._policy_ids )
764 ndx = idlist.index( policy_id )
765 idlist = idlist[ :ndx ] + idlist[ ndx+1: ]
766 self._policy_ids = tuple( idlist )
769 #
770 # 'portal_caching' interface methods
771 #
772 security.declareProtected( View, 'getHTTPCachingHeaders' )
773 def getHTTPCachingHeaders( self, content, view_method, keywords, time=None):
774 """
775 Return a list of HTTP caching headers based on 'content',
776 'view_method', and 'keywords'.
777 """
778 context = createCPContext( content, view_method, keywords, time=time )
779 for policy_id, policy in self.listPolicies():
781 headers = policy.getHeaders( context )
782 if headers:
783 return headers
785 return ()
787 security.declareProtected( View, 'getModTimeAndETag' )
788 def getModTimeAndETag( self, content, view_method, keywords, time=None):
789 """ Return the modification time and ETag for the content object,
790 view method, and keywords as the tuple (modification_time, etag,
791 set_last_modified_header), where modification_time is a DateTime,
792 or None.
793 """
794 context = createCPContext( content, view_method, keywords, time=time )
795 for policy_id, policy in self.listPolicies():
796 if policy.getEnable304s() and policy.testPredicate(context):
798 last_modified = policy._mtime_func(context)
799 if type(last_modified) is type(''):
800 last_modified = DateTime(last_modified)
802 content_etag = None
803 if policy.getETagFunc():
804 content_etag = policy._etag_func(context)
806 return (last_modified, content_etag, policy.getLastModified())
808 return None
811 InitializeClass( CachingPolicyManager )
814 def manage_addCachingPolicyManager( self, REQUEST=None ):
815 """
816 Add a CPM to self.
817 """
818 id = CachingPolicyManager.id
819 mgr = CachingPolicyManager()
820 self._setObject( id, mgr )
822 if REQUEST is not None:
823 REQUEST[ 'RESPONSE' ].redirect( self.absolute_url()
824 + '/manage_main'
825 + '?manage_tabs_message=Caching+Policy+Manager+added.'
826 )