source: products/vendor/Products.CacheSetup/current/Products/CacheSetup/content/header_set.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.8 KB
Line 
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# This file contains code drawn from CMFCore's CachingPolicyManager.py
14"""Header set implementation
15
16$Id: $
17"""
18
19__authors__ = 'Geoff Davis <geoff@geoffdavis.net>'
20__docformat__ = 'restructuredtext'
21
22from zope.tales.tales import CompilerError
23
24from AccessControl import ClassSecurityInfo
25from App.Common import rfc1123_date
26from DateTime import DateTime
27from Products.PageTemplates.Expressions import getEngine
28
29from Products.CMFCore.utils import getToolByName
30from Products.CMFCore import permissions
31
32from Products.Archetypes.atapi import BaseContent
33from Products.Archetypes.atapi import DisplayList
34from Products.Archetypes.atapi import registerType
35from Products.Archetypes.atapi import Schema
36
37from Products.Archetypes.atapi import BooleanField
38from Products.Archetypes.atapi import IntegerField
39from Products.Archetypes.atapi import StringField
40from Products.Archetypes.atapi import TextField
41from Products.Archetypes.atapi import LinesField
42
43from Products.Archetypes.atapi import BooleanWidget
44from Products.Archetypes.atapi import IntegerWidget
45from Products.Archetypes.atapi import SelectionWidget
46from Products.Archetypes.atapi import TextAreaWidget
47from Products.Archetypes.atapi import StringWidget
48
49from Products.CacheSetup.config import PROJECT_NAME, CACHE_TOOL_ID
50from nocatalog import NoCatalog
51
52schema = BaseContent.schema.copy() + Schema((
53
54    TextField(
55        'description',
56        required=0,
57        allowable_content_types = ('text/plain',),
58        default='A set of HTTP headers to be assigned with a caching rule',
59        write_permission = permissions.ManagePortal,
60        widget=TextAreaWidget(
61            label='Description',
62            cols=60,
63            rows=5,
64            description='Basic documentation for this caching assignment policy')),
65
66    BooleanField(
67        'pageCache',
68        default=False,
69        write_permission = permissions.ManagePortal,
70        widget=BooleanWidget(
71            label='Cache Templates in Memory',
72            description='Should templates with this header set be cached in the page cache?')),
73
74    StringField(
75        'lastModified',
76        default='yes',
77        write_permission = permissions.ManagePortal,
78        vocabulary=DisplayList((
79            ('yes', 'Yes'), 
80            ('no', 'No'), 
81            ('delete', 'No, and delete any pre-existing header'))),
82        widget=SelectionWidget(
83            label='Last-Modified Header',
84            description='Should the header set add a Last-Modified header?  Note: There appears '
85                        'to be an IE bug that causes incorrect caching behavior when you have both '
86                        'a Last-Modified header (used for caching heuristics) and explicit caching '
87                        'headers such as max-age=0.  The problem may arise from setting the Expires '
88                        'header to the current time.  One workaround is to not set the Last-Modified '
89                        'header and to delete any pre-existing Last-Modified header.  Setting Expires '
90                        'to a time in the past may also work (this is now the default behavior if '
91                        'max-age=0) -- further testing is needed.')),
92    # The last_modified argument is used to determine whether to add a
93    # Last-Modified header.  last_modified=1 by default.  There appears
94    # to be a bug in IE 6 (and possibly other versions) that uses the
95    # Last-Modified header plus some heuristics rather than the other
96    # explicit caching headers to determine whether to render content
97    # from the cache.  If you set, say, max-age=0, must-revalidate and
98    # have a Last-Modified header some time in the past, IE will
99    # recognize that the page in cache is stale and will request an
100    # update from the server BUT if you have a Last-Modified header
101    # with an older date, will then ignore the update and render from
102    # the cache, so you may want to disable the Last-Modified header
103    # when controlling caching using Cache-Control headers.
104    #
105    # Update: The problem may be because we were setting Expires to
106    # the current time when max-age was set to 0.  IE may have Expires
107    # override max-age (bad) or something similar.  We are now setting
108    # Expires to a time in the past if max-age is 0.
109
110    BooleanField(
111        'etag',
112        default=True,
113        write_permission = permissions.ManagePortal,
114        widget=BooleanWidget(
115            label='ETag Header',
116            description='Should the header set add an ETag header?')),
117
118    BooleanField(
119        'enable304s',
120        default=True,
121        write_permission = permissions.ManagePortal,
122        widget=BooleanWidget(
123            label='Enable 304s',
124            description='Should objects that support conditional GET handling with ETags '
125                        '(FSPageTemplates) be allowed to return a 304 Not Modified HTTP status?')),
126
127    BooleanField(
128        'vary',
129        default=True,
130        write_permission = permissions.ManagePortal,
131        widget=BooleanWidget(
132            label='Vary Header',
133            description='Should the header set add a Vary header?')),
134
135    IntegerField(
136        'maxAge',
137        default=None,
138        write_permission = permissions.ManagePortal,
139        widget=IntegerWidget(
140            label='Cache-Control Header: max-age',
141            size=6,
142            description='The amount of time in seconds that a page can be cached on a '
143                        'client without revalidation.  Also used to set the Expires header '
144                        '(HTTP 1.0).  If left blank, no max-age token will be added and no '
145                        'Expires header will be set.')),
146
147    IntegerField(
148        'sMaxAge',
149        default=None,
150        write_permission = permissions.ManagePortal,
151        widget=IntegerWidget(
152            label='Cache-Control Header: s-maxage',
153            size=6,
154            description='The amount of time in seconds that a page can be cached in a proxy '
155                        'cache without revalidation.  If left blank, no s-maxage token will '
156                        'be added.  If you do not indicate that you have a purgeable proxy '
157                        'server available (e.g. squid), the s-maxage token will not be added.')),
158
159    BooleanField(
160        'mustRevalidate',
161        default=False,
162        write_permission = permissions.ManagePortal,
163        widget=BooleanWidget(
164            label='Cache-Control Header: must-revalidate',
165            description='Must objects be revalidated with the server (by a conditional GET) '
166                        'once they have expired in the client?')),
167
168    BooleanField(
169        'proxyRevalidate',
170        default=False,
171        write_permission = permissions.ManagePortal,
172        widget=BooleanWidget(
173            label='Cache-Control Header: proxy-revalidate',
174            description='Must objects be revalidated with the server once they have expired '
175                        'in the proxy cache?')),
176
177    BooleanField(
178        'noCache',
179        default=False,
180        write_permission = permissions.ManagePortal,
181        widget=BooleanWidget(
182            label='Cache-Control Header: no-cache',
183            description='If no-cache is set, objects may be cached, but the cache must '
184                        'revalidate with the server before serving the cached version. '
185                        'Setting no-cache also causes a Pragma: no-cache (HTTP 1.0) header '
186                        'to be sent. (Caution: There is a Microsoft IE bug with HTTPS and '
187                        'no-cache so this token should probably be avoided if the server '
188                        'supports SSL connections)')),
189
190    BooleanField(
191        'noStore',
192        default=False,
193        write_permission = permissions.ManagePortal,
194        widget=BooleanWidget(
195            label='Cache-Control Header: no-store',
196            description='If no-store is set, objects may not be stored in a cache. '
197                        '(Caution: There is a Microsoft IE bug with HTTPS and no-store '
198                        'so this token should probably be avoided if the server '
199                        'supports SSL connections)')),
200
201    BooleanField(
202        'public',
203        default=False,
204        write_permission = permissions.ManagePortal,
205        widget=BooleanWidget(
206            label='Cache-Control Header: public',
207            description='If the public flag is set, proxy caches may cache a page even '
208                        'in the presence of things like Authentication headers that would '
209                        'otherwise prevent them from doing so.')),
210
211    BooleanField(
212        'private',
213        default=False,
214        write_permission = permissions.ManagePortal,
215        widget=BooleanWidget(
216            label='Cache-Control Header: private',
217            description='If the private flag is set, shared HTTP 1.1-compliant proxy caches '
218                        'will not cache a page.  The non-shared browser cache, however, may '
219                        'cache a page with the private flag.')),
220
221    BooleanField(
222        'noTransform',
223        default=False,
224        write_permission = permissions.ManagePortal,
225        widget=BooleanWidget(
226            label='Cache-Control Header: no-transform',
227            description='The no-transform flag tells intermediate proxies not to alter the '
228                        'content in transit (e.g. tell proxies to mobile phones not to '
229                        'downsample images, etc)')),
230
231    IntegerField(
232        'preCheck',
233        default=None,
234        write_permission = permissions.ManagePortal,
235        widget=IntegerWidget(
236            label='Cache-Control Header: pre-check',
237            description='<strong>IE-ONLY:</strong> '
238                        'The pre-check flag is a Microsoft IE Cache-Control extension. '
239                        'See http://msdn.microsoft.com/workshop/author/perf/perftips.asp for details. '
240                        'IE 5+ will send some cache directives unless pre-check and post-check '
241                        'are set to 0.')),
242
243    IntegerField(
244        'postCheck',
245        default=None,
246        write_permission = permissions.ManagePortal,
247        widget=IntegerWidget(
248            label='Cache-Control Header: post-check',
249            description='<strong>IE-ONLY:</strong> '
250                        'The post-check flag is a Microsoft IE Cache-Control extension. '
251                        'See http://msdn.microsoft.com/workshop/author/perf/perftips.asp for details. '
252                        'IE 5+ will send some cache directives unless pre-check and post-check '
253                        'are set to 0.')),
254
255    IntegerField(
256        'staleWhileRevalidate',
257        default=None,
258        write_permission = permissions.ManagePortal,
259        widget=IntegerWidget(
260            label='Cache-Control Header: stale-while-revalidate',
261            size=6,
262            description='<strong>EXPERIMENTAL:</strong> '
263                        'The amount of time in seconds that a stale page may be served from a proxy '
264                        'cache while the proxy revalidates the page asynchronously. '
265                        'See http://www.mnot.net/blog/2007/12/12/stale for details.')),
266
267    IntegerField(
268        'staleIfError',
269        default=None,
270        write_permission = permissions.ManagePortal,
271        widget=IntegerWidget(
272            label='Cache-Control Header: stale-if-error',
273            size=6,
274            description='<strong>EXPERIMENTAL:</strong> '
275                        'The amount of time in seconds that a stale page may be served from a proxy '
276                        'cache if revalidation results in an error. '
277                        'See http://www.mnot.net/blog/2007/12/12/stale for details')),
278
279    LinesField(
280        'extraHeader',
281        write_permission = permissions.ManagePortal,
282        widget=StringWidget(
283            macro_edit='extraHeaderWidget',
284            label='Extra Response Header',
285            size=80,
286            description='<strong>EXPERIMENTAL:</strong> '
287                        'An extra header that will be added to the response. '
288                        'Two header types are allowed: "Surrogate-Control: &lt;value&gt;" '
289                        'and "X-&lt;name&gt'': &lt;value&gt;". '
290                        'Enter the entire header line like so: "X-My-Header: Hello World" ')),
291
292
293))
294
295schema['id'].widget.ignore_visible_ids=True                       
296schema['id'].widget.description="Should not contain spaces, underscores or mixed case. An 'X-Header-Set-Id' header with this id will be added."
297
298class HeaderSet(NoCatalog, BaseContent):
299    """A content object used for setting a set of response headers for a page"""
300
301    security = ClassSecurityInfo()
302    archetype_name = 'Response Header Set'
303    portal_type = meta_type = 'HeaderSet'
304    schema = schema
305    global_allow = 0
306    _at_rename_after_creation = True
307
308    actions = (
309        {'action':      'string:$object_url',
310         'category':    'object',
311         'id':          'view',
312         'name':        'Cache Setup',
313         'permissions': (permissions.ManagePortal,),
314         'visible':     False},
315    )
316
317    aliases = {
318        '(Default)':    'cache_policy_item_config',
319        'view' :        'cache_policy_item_config',
320        'edit' :        'cache_policy_item_config'
321    }
322
323    def setExtraHeader(self, value):
324        """Clean up syntax to make a valid header"""
325        if isinstance(value, basestring):
326            value = value.split()
327        value = " ".join(value) + ':'
328        key, value = [v.strip() for v in value.split(':')[:2]]
329        if key and value == '':
330            # Lines field doesn't like empty strings
331            value = 'blank'
332        key = ''.join([k.isalnum() and k.lower() or '-' for k in key])
333        if key and not key.startswith('x-') and key != 'surrogate-control':
334            key = 'x-' + key
335        # Convert to standard HTTP header case style
336        # Copied from ZServer.HTTPResponse
337        key="%s%s" % (key[:1].upper(), key[1:])
338        start=0
339        l=key.find('-',start)
340        while l >= start:
341            key="%s-%s%s" % (key[:l],key[l+1:l+2].upper(),key[l+2:])
342            start=l+1
343            l=key.find('-',start)
344        self.getField('extraHeader').set(self, (key, value))
345
346    def _validate_expression(self, expression):
347        try:
348            getEngine().compile(expression)
349        except CompilerError, e:
350            return 'Bad expression:', str(e)
351        except:
352            raise
353
354    def getLastModifiedValue(self, expr_context):
355        if not self.getLastModified() == 'yes':
356            return None # no last-modified value if disabled
357        rule = expr_context.vars['rule']
358        return rule.getLastModified(expr_context)
359
360    def getVaryValue(self, expr_context):
361        if not self.getVary():
362            return None # no Vary value if disabled
363        rule = expr_context.vars['rule']
364        return rule.getVary(expr_context)
365
366    def getPageCacheKey(self, expr_context):
367        # Return a cache key for use with PageCacheManager
368        if not self.getPageCache():
369            return None
370        return self._getEtagValue(expr_context)
371
372    def _getEtagValue(self, expr_context):
373        rule = expr_context.vars['rule']
374        request = expr_context.vars['request']
375        object = expr_context.vars['object']
376        view = expr_context.vars['view']
377        member = expr_context.vars['member']
378        return rule.getEtag(request, object, view, member)
379
380    def getEtagValue(self, expr_context):
381        if not self.getEtag():
382            return None # no etag if disabled
383        return self._getEtagValue(expr_context)
384
385    def getHeaders(self, expr_context):
386        """Returns a list of caching headers in (key, value) tuples"""
387        pcs = getToolByName(self, CACHE_TOOL_ID)
388        headers_to_add = []
389        headers_to_remove = []
390
391        last_modified = self.getLastModified()
392        if last_modified == 'yes':
393            mod_time = self.getLastModifiedValue(expr_context)
394            if type(mod_time) is type(''):
395                mod_time = DateTime(mod_time)
396            if mod_time is not None:
397                mod_time_st = rfc1123_date(mod_time.timeTime())
398                headers_to_add.append(('Last-modified', mod_time_st))
399        elif last_modified == 'delete':
400            headers_to_remove.append('Last-modified')
401
402        if self.getEtag():
403            etag = self.getEtagValue(expr_context)
404            if etag is not None:
405                headers_to_add.append(('ETag', etag))
406
407        vary = self.getVaryValue(expr_context)
408        if vary:
409            if pcs.getGzip() in ('accept-encoding', 'accept-encoding+user-agent'):
410                # Zope adds Accept-Encoding automatically
411                vary = ', '.join([v.strip() for v in vary.split(',') if v.strip() != 'Accept-Encoding'])
412            if vary:
413                headers_to_add.append(('Vary', vary))
414
415        # a list of cache-control tokens
416        control = []
417
418        max_age = self.getMaxAge()
419        if max_age is not None:
420            now = expr_context.vars['time']
421            if max_age > 0:
422                expiration_time = now.timeTime() + max_age
423            else:
424                # immediate expiration requires that the client clock be precisely synchronized
425                # since this is not guaranteed, we'll set the expiration time to 10 years ago
426                expiration_time = now.timeTime() - 10*365*24*3600
427            exp_time_st = rfc1123_date(expiration_time)
428            headers_to_add.append(('Expires', exp_time_st))
429            control.append('max-age=%d' % max_age)
430
431        s_max_age = self.getSMaxAge()
432        if s_max_age is not None:
433            control.append('s-maxage=%d' % s_max_age)
434
435        if self.getNoCache():
436            control.append('no-cache')
437            # The following is for HTTP 1.0 clients
438            headers_to_add.append(('Pragma', 'no-cache'))
439
440        if self.getNoStore():
441            control.append('no-store')
442
443        if self.getPublic():
444            control.append('public')
445
446        if self.getPrivate():
447            control.append('private')
448
449        if self.getMustRevalidate():
450            control.append('must-revalidate')
451
452        if self.getProxyRevalidate():
453            control.append('proxy-revalidate')
454
455        if self.getNoTransform():
456            control.append('no-transform')
457
458        pre_check = self.getPreCheck()
459        if pre_check is not None:
460            control.append('pre-check=%d' % pre_check)
461
462        post_check = self.getPostCheck()
463        if post_check is not None:
464            control.append('post-check=%d' % post_check)
465
466        stale_while_revalidate = self.getStaleWhileRevalidate()
467        if stale_while_revalidate is not None:
468            control.append('stale-while-revalidate=%d' % stale_while_revalidate)
469
470        stale_if_error = self.getStaleIfError()
471        if stale_if_error is not None:
472            control.append('stale-if-error=%d' % stale_if_error)
473
474        if control:
475            headers_to_add.append(('Cache-control', ', '.join(control)))
476
477        extra = self.getExtraHeader()
478        if extra is not ():
479            headers_to_add.append(extra)
480
481        # add debugging information
482        rule = expr_context.vars['rule']
483        headers_to_add.append(('X-Caching-Rule-Id', rule.getId()))
484        headers_to_add.append(('X-Header-Set-Id', self.getId()))
485
486        return (headers_to_add, headers_to_remove)
487
488registerType(HeaderSet, PROJECT_NAME)
Note: See TracBrowser for help on using the repository browser.