source: products/vendor/Products.CacheSetup/current/Products/CacheSetup/content/base_cache_rule.py @ 3296

Last change on this file since 3296 was 3296, checked in by fenix, 13 years ago

Load Products.CacheSetup?-1.2.1 into vendor/Products.CacheSetup?/current.

  • Property svn:eol-style set to native
File size: 19.5 KB
Line 
1"""
2BaseCacheRule is the base class for ContentCacheRule,
3PolicyHTTPCacheManagerCacheRule and TemplateCacheRule.
4
5$Id: base_cache_rule.py 86306 2009-05-17 23:15:20Z newbery $
6"""
7
8__authors__ = 'Geoff Davis <geoff@geoffdavis.net>'
9__docformat__ = 'restructuredtext'
10
11from urllib import quote
12
13from zope.component import getMultiAdapter
14from zope.interface import implements
15from zope.tales.tales import CompilerError
16from zExceptions import NotFound
17
18from Acquisition import aq_inner
19from DateTime import DateTime
20from AccessControl import ClassSecurityInfo
21from AccessControl.User import nobody
22from Products.PageTemplates.Expressions import getEngine, SecureModuleImporter
23from ZopeUndo.Prefix import Prefix
24
25from Products.Archetypes.interfaces import IBaseContent
26from Products.Archetypes.atapi import BaseContent
27from Products.Archetypes.atapi import DisplayList
28from Products.Archetypes.atapi import registerType
29from Products.Archetypes.atapi import Schema
30
31from Products.Archetypes.atapi import IntegerField
32from Products.Archetypes.atapi import LinesField
33from Products.Archetypes.atapi import StringField
34
35from Products.Archetypes.atapi import IntegerWidget
36from Products.Archetypes.atapi import LinesWidget
37from Products.Archetypes.atapi import MultiSelectionWidget
38from Products.Archetypes.atapi import SelectionWidget
39from Products.Archetypes.atapi import StringWidget
40
41from Products.CMFCore import Expression
42from Products.CMFCore.utils import getToolByName
43from Products.CMFCore import permissions
44
45from Products.CMFPlone.interfaces import IBrowserDefault
46
47from Products.CacheSetup.utils import base_hasattr
48from Products.CacheSetup.config import *
49from Products.CacheSetup.interfaces import ICacheRule
50from nocatalog import NoCatalog
51
52schema = BaseContent.schema.copy()
53
54header_set_schema = Schema((
55
56    LinesField(
57        'cacheStop',
58        default=('portal_status_message','statusmessages'),
59        write_permission = permissions.ManagePortal,
60        widget=LinesWidget(
61            label='Cache Preventing Request Values',
62            description='Values in the request that prevent caching if present')),
63
64    StringField(
65        'predicateExpression',
66        required=0,
67        edit_accessor='getPredicateExpression',
68        write_permission = permissions.ManagePortal,
69        widget=StringWidget(
70            label='Predicate',
71            size=80,
72            description='A TALES expression for determining whether this rule applies. '
73                        'Available variables = request, object, view (ID of the current '
74                        'template), member (None if anonymous).')),
75
76    StringField(
77        'headerSetIdAnon',
78        default='cache-with-etag',
79        vocabulary='getHeaderSetVocabulary',
80        enforce_vocabulary = 1,
81        write_permission = permissions.ManagePortal,
82        widget=SelectionWidget(
83            label='Header Set for Anonymous Users',
84            description='Header set for anonymous users.')),
85
86    StringField(
87        'headerSetIdAuth',
88        default='cache-with-etag',
89        vocabulary='getHeaderSetVocabulary',
90        enforce_vocabulary = 1,
91        write_permission = permissions.ManagePortal,
92        widget=SelectionWidget(
93            label='Header Set for Authenticated Users',
94            description='Header set for authenticated users.')),
95
96    StringField(
97        'headerSetIdExpression',
98        required=0,
99        edit_accessor='getHeaderSetIdExpression',
100        write_permission = permissions.ManagePortal,
101        widget=StringWidget(
102            label='Header Set Expression',
103            size=80,
104            description='A TALES expression that returns the ID of the header set '
105                        'to be used.  Applied if the header set specified above is '
106                        'set to "Expression".  Available variables = request, object, '
107                        'view (ID of the current template), member (None if anonymous)')),
108
109    StringField(
110        'lastModifiedExpression',
111        default='python:object.modified()',
112        edit_accessor='getLastModifiedExpression',
113        write_permission = permissions.ManagePortal,
114        widget=StringWidget(
115            label='Last-Modified Expression',
116            size=80,
117            description='An expression used to obtain the last-modified time for '
118                        'the page.  Used in setting the Last-Modified header')),
119
120    StringField(
121        'varyExpression',
122        required=0,
123        default='python: rule.portal_cache_settings.getVaryHeader()',
124        edit_accessor='getVaryExpression',
125        write_permission = permissions.ManagePortal,
126        widget=StringWidget(
127            label='Vary Expression',
128            size=80,
129            description='A TALES expression that will be used to generate the Vary '
130                        'header.  Available values: request, object, view '
131                        '(the template ID), member (None if Anonymous)')),
132    ))
133
134etag_schema = Schema((
135
136    LinesField(
137        'etagComponents',
138        default=('member','skin','language','gzip','catalog_modified'),
139        multiValued=1,
140        enforce_vocabulary = 1,
141        write_permission = permissions.ManagePortal,
142        vocabulary=DisplayList((
143            ('member','Current member\'s ID'),
144            ('roles','Current member\'s roles'),
145            ('permissions','Current member\'s permissions'),
146            ('skin','Current skin'),
147            ('language','Browser\'s preferred language'),
148            ('user_language','User\'s preferred language'),
149            ('gzip','Browser can receive gzipped content'),
150            ('object_locked', 'Context lock state'),
151            ('last_modified','Context modification time'),
152            ('catalog_modified', 'Time of last catalog change'))),
153        widget=MultiSelectionWidget(
154            label='ETag Components',
155            format='checkbox',
156            description='Items used to construct the ETag')),
157
158    LinesField(
159        'etagRequestValues',
160        default=(),
161        write_permission = permissions.ManagePortal,
162        widget=LinesWidget(
163            label='ETag Request Values',
164            description='Request values used to construct the ETag')),
165
166    IntegerField(
167        'etagTimeout',
168        required=0,
169        default=3600,
170        write_permission = permissions.ManagePortal,
171        widget=IntegerWidget(
172            label='ETag Timeout',
173            description='Maximum amount of time an ETag is valid (leave blank for forever)')),
174
175    StringField(
176        'etagExpression',
177        required=0,
178        edit_accessor='getEtagExpression',
179        write_permission = permissions.ManagePortal,
180        widget=StringWidget(
181            label='ETag Expression',
182            size=80,
183            description='A TALES expression that will be appended to the ETag '
184                        'generated with the above settings when "The expression '
185                        'below" is selected.  Available values: request, object, '
186                        'view (the template ID), member (None if Anonymous)')),
187    ))
188
189class BaseCacheRule(NoCatalog, BaseContent):
190    """
191    """
192    __implements__ = ICacheRule, BaseContent.__implements__
193    implements(IBaseContent)
194
195    security = ClassSecurityInfo()
196    archetype_name = 'Base Cache Rule'
197    portal_type = meta_type = 'BaseCacheRule'
198    schema = schema
199    global_allow = 0
200    _at_rename_after_creation = True
201
202    def _validate_expression(self, expression):
203        try:
204            getEngine().compile(expression)
205        except CompilerError, e:
206            return 'Bad expression:', str(e)
207        except:
208            raise
209
210    def getPredicateExpression(self):
211        expression = self.getField('predicateExpression').get(self)
212        if expression:
213            return expression.text
214       
215    def setPredicateExpression(self, expression):
216        if expression is None:
217            expression = ''
218        return self.getField('predicateExpression').set(self, Expression.Expression(expression))
219
220    def validate_predicateExpression(self, expression):
221        return self._validate_expression(expression)
222
223    def testPredicate(self, expr_context):
224        expression = self.getField('predicateExpression').get(self)
225        if expression:
226            if not expression.text:  # empty expression
227                return True
228            return expression(expr_context)
229
230    def getEtagExpression(self):
231        expression = self.getField('etagExpression').get(self)
232        if expression:
233            return expression.text
234       
235    def setEtagExpression(self, expression):
236        if expression is None:
237            expression = ''
238        return self.getField('etagExpression').set(self, Expression.Expression(expression))
239
240    def validate_etagExpression(self, expression):
241        return self._validate_expression(expression)
242
243    def getEtagExpressionValue(self, expr_context):
244        expression = self.getField('etagExpression').get(self)
245        if expression:
246            return expression(expr_context)
247
248    def getHeaderSetIdExpression(self):
249        expression = self.getField('headerSetIdExpression').get(self)
250        if expression:
251            return expression.text
252       
253    def setHeaderSetIdExpression(self, expression):
254        if expression is None:
255            expression = ''
256        return self.getField('headerSetIdExpression').set(self, Expression.Expression(expression))
257
258    def validate_headerSetIdExpression(self, expression):
259        return self._validate_expression(expression)
260
261    def getHeaderSetIdExpressionValue(self, expr_context):
262        expression = self.getField('headerSetIdExpression').get(self)
263        if expression:
264            return expression(expr_context)
265
266    def getLastModifiedExpression(self):
267        expression = self.getField('lastModifiedExpression').get(self)
268        if expression:
269            return expression.text
270       
271    def setLastModifiedExpression(self, expression):
272        if expression is None:
273            expression = ''
274        return self.getField('lastModifiedExpression').set(self, Expression.Expression(expression))
275
276    def validate_lastModifiedExpression(self, expression):
277        return self._validate_expression(expression)
278
279    def getLastModified(self, expr_context):
280        expression = self.getField('lastModifiedExpression').get(self)
281        if expression:
282            return expression(expr_context)
283
284    def lastDate(self, *dates):
285        if len(dates) == 0:
286            return self.portal_cache_settings.getChangeDate()
287        dates = list(dates)
288        timeout = self.getEtagTimeout()
289        if timeout:
290            time = DateTime()
291            time = timeout * (int(time.timeTime()/timeout) - 1)
292            time = DateTime(time)
293            dates.append(time)
294        dates.sort()
295        return dates[-1]
296
297    def getLastTransactionDate(self, context=None):
298        spec = {}
299        if context is None:
300            context = getToolByName(self, 'portal_url').getPortalObject()
301        path = '/'.join(context.getPhysicalPath())
302        spec['description'] = Prefix(path)
303        lastTransaction = self._p_jar.db().undoInfo(0, 1, spec)
304        if len(lastTransaction) == 0:
305            return DateTime(self.Control_Panel.process_start)
306        return DateTime(lastTransaction[0]['time'])
307
308    def getVaryExpression(self):
309        expression = self.getField('varyExpression').get(self)
310        if expression:
311            return expression.text
312       
313    def setVaryExpression(self, expression):
314        if expression is None:
315            expression = ''
316        return self.getField('varyExpression').set(self, Expression.Expression(expression))
317
318    def validate_varyExpression(self, expression):
319        return self._validate_expression(expression)
320
321    def getVary(self, expr_context):
322        expression = self.getField('varyExpression').get(self)
323        if expression:
324            return expression(expr_context)
325
326    def _getExpressionContext(self, request, object, view, member, keywords=(), time=None):
327        """Construct an expression context for TALES expressions used in cache rules and header sets"""
328        if time is None:
329            time = DateTime()
330        data = {'rule'     : self,
331                'request'  : request,
332                'object'   : object,
333                'view'     : view,
334                'member'   : member,
335                'time'     : time,
336                'keywords' : keywords,
337                'modules'  : SecureModuleImporter,
338                'nothing'  : None
339               }
340        return getEngine().getContext(data)
341
342    def _associateTemplate(self, object, template_id):
343        try:
344            template = object.unrestrictedTraverse(template_id)
345        except (AttributeError, KeyError, NotFound):
346            template = None 
347            # XXX should log the fact that this template can't be found
348        if template is not None:
349            manager_id = getattr(template, 'ZCacheable_getManagerId', None)
350            if manager_id is not None:
351                if manager_id() is None:
352                    template.ZCacheable_setManagerId(PAGE_CACHE_MANAGER_ID)
353
354    def _getHeaderSet(self, request, object, view, member):
355        stop_items = self.getCacheStop()
356        if stop_items:
357            for item in stop_items:
358                if request.get(item, None) is not None:
359                    return None
360               
361        expr_context = self._getExpressionContext(request, object, view, member)
362        if not self.testPredicate(expr_context):
363            return None
364               
365        if member is None:
366            header_set_id = self.getHeaderSetIdAnon()
367        else:
368            header_set_id = self.getHeaderSetIdAuth()
369        if header_set_id == 'expression':
370            header_set_id = self.getHeaderSetIdExpressionValue(expr_context)
371        if header_set_id == 'None':
372            header_set_id = None
373        if header_set_id:
374            pcs = getToolByName(self, CACHE_TOOL_ID)
375            return pcs.getHeaderSetById(header_set_id)
376
377    def getHeaderSetVocabulary(self):
378        pcs = getToolByName(self, CACHE_TOOL_ID)
379        display_id = pcs.getDisplayPolicy().getId()
380        headers = pcs.getHeaderSets(display_id)
381        vocabulary = [('expression', 'Use expression below')] + \
382                     [(hs.getId(), hs.Title() or hs.getId()) 
383                      for hs in headers.objectValues()] + \
384                     [('None', 'Rule does not apply')]
385        return DisplayList(tuple(vocabulary))
386       
387    def getObjectDefaultView(self, obj):
388        """Get the id of an object's default view"""
389        context = aq_inner(obj)
390        browserDefault = IBrowserDefault(context, None)
391       
392        if browserDefault is not None:
393            try:
394                return browserDefault.defaultView()
395            except AttributeError:
396                # Might happen if FTI didn't migrate yet.
397                pass
398
399        fti = context.getTypeInfo()
400        try:
401            # XXX: This isn't quite right since it assumes the action starts with ${object_url}
402            action = fti.getActionInfo('object/view')['url'].split('/')[-1]
403        except ValueError:
404            # If the action doesn't exist, stop
405            return None
406
407        # Try resolving method aliases because we need a real template_id here
408        if action:
409            action = fti.queryMethodID(action, default = action, context = context)
410        else:
411            action = fti.queryMethodID('(Default)', default = action, context = context)
412
413        # Strip off leading / and/or @@
414        if action and action[0] == '/':
415            action = action[1:]
416        if action and action.startswith('@@'):
417            action = action[2:]
418        return action
419       
420    def _addEtagComponent(self, etag, component):
421        etag += '|' + str(component).replace(',',';')  # commas are used to separate etags in if-none-match headers
422        return etag
423
424    security.declarePublic('getEtag')
425    def getEtag(self, request, object, view, member, time=None):
426        # note: member may come in as None if the member is anonymous
427        portal = getToolByName(self, 'portal_url').getPortalObject()
428        pcs = getattr(portal, CACHE_TOOL_ID)
429        etag = ''
430        values = self.getEtagComponents()
431        if 'member' in values:
432            if member is not None:
433                username = member.getUserName()
434            else:
435                username = ''
436            etag = self._addEtagComponent(etag, username)
437        if 'roles' in values or 'permissions' in values:
438            m = member
439            if m is None:
440                m = portal.portal_membership.wrapUser(nobody)
441            roles = list(m.getRolesInContext(object))
442            roles.sort()
443            etag = self._addEtagComponent(etag, ';'.join(roles))
444            if 'permissions' in values:
445                etag = self._addEtagComponent(etag, pcs.getPermissionCount())
446        if 'skin' in values:
447            try:
448                skin_name = self.getCurrentSkinName()
449            except AttributeError:
450                stool = getToolByName(self, 'portal_skins')
451                skin_name = self.getSkinNameFromRequest(request)
452               
453                if skin_name is None:
454                    # Use default skin
455                    skin_name = stool.getDefaultSkin()
456
457            etag = self._addEtagComponent(etag, skin_name)
458        if 'language' in values:
459            etag = self._addEtagComponent(etag, request.get('HTTP_ACCEPT_LANGUAGE', ''))
460        if 'user_language' in values:
461            ltool = getToolByName(self, 'portal_languages', None)
462            if ltool is None:
463                ptool = getToolByName(self, 'portal_properties')
464                lang = ptool.site_properties.default_language
465            else:
466                lang = ltool.getPreferredLanguage()
467            etag = self._addEtagComponent(etag, lang)
468        if 'gzip' in values:
469            (enable_compression, force, gzip_capable) = pcs.isGzippable(0, 0, request)
470            etag = self._addEtagComponent(etag, int(force or (enable_compression and gzip_capable)))
471        if 'last_modified' in values:
472            etag = self._addEtagComponent(etag, object.modified().timeTime())
473        if 'catalog_modified' in values:
474            etag = self._addEtagComponent(etag, pcs.getCatalogCount())
475        if 'object_locked' in values:
476           if PLONE25:
477               lockable = hasattr(aq_inner(object).aq_explicit, 'wl_isLocked')
478               isLocked = lockable and object.wl_isLocked()
479           else:
480               context_state = getMultiAdapter((object, request), name=u'plone_context_state')
481               isLocked = context_state.is_locked()
482           etag = self._addEtagComponent(etag, isLocked)
483        if self.getEtagExpression():
484            expr_context = self._getExpressionContext(request, object, view, member)
485            etag = self._addEtagComponent(etag, self.getEtagExpressionValue(expr_context))
486           
487        marker = []
488        req_values = self.getEtagRequestValues()
489        if req_values:
490            for rv in req_values:
491                v = request.get(rv, marker)
492                if v is not marker:
493                    etag = self._addEtagComponent(etag, quote(str(v)))
494                else:
495                    etag = self._addEtagComponent(etag, '')
496                   
497        timeout = self.getEtagTimeout()
498        if timeout:
499            if time is None:
500                time = DateTime()
501            etag = self._addEtagComponent(etag, int(time.timeTime()/timeout))
502        return etag
503
504    security.declarePublic('getRelativeUrlsToPurge')
505    def getRelativeUrlsToPurge(self, object, urls):
506        return urls
507
508registerType(BaseCacheRule, PROJECT_NAME)
509
510__all__ = (
511    'header_set_schema',
512    'etag_schema',
513    'BaseCacheRule',
514)
Note: See TracBrowser for help on using the repository browser.