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 | |
---|
22 | from zope.tales.tales import CompilerError |
---|
23 | |
---|
24 | from AccessControl import ClassSecurityInfo |
---|
25 | from App.Common import rfc1123_date |
---|
26 | from DateTime import DateTime |
---|
27 | from Products.PageTemplates.Expressions import getEngine |
---|
28 | |
---|
29 | from Products.CMFCore.utils import getToolByName |
---|
30 | from Products.CMFCore import permissions |
---|
31 | |
---|
32 | from Products.Archetypes.atapi import BaseContent |
---|
33 | from Products.Archetypes.atapi import DisplayList |
---|
34 | from Products.Archetypes.atapi import registerType |
---|
35 | from Products.Archetypes.atapi import Schema |
---|
36 | |
---|
37 | from Products.Archetypes.atapi import BooleanField |
---|
38 | from Products.Archetypes.atapi import IntegerField |
---|
39 | from Products.Archetypes.atapi import StringField |
---|
40 | from Products.Archetypes.atapi import TextField |
---|
41 | from Products.Archetypes.atapi import LinesField |
---|
42 | |
---|
43 | from Products.Archetypes.atapi import BooleanWidget |
---|
44 | from Products.Archetypes.atapi import IntegerWidget |
---|
45 | from Products.Archetypes.atapi import SelectionWidget |
---|
46 | from Products.Archetypes.atapi import TextAreaWidget |
---|
47 | from Products.Archetypes.atapi import StringWidget |
---|
48 | |
---|
49 | from Products.CacheSetup.config import PROJECT_NAME, CACHE_TOOL_ID |
---|
50 | from nocatalog import NoCatalog |
---|
51 | |
---|
52 | schema = 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: <value>" ' |
---|
289 | 'and "X-<name>'': <value>". ' |
---|
290 | 'Enter the entire header line like so: "X-My-Header: Hello World" ')), |
---|
291 | |
---|
292 | |
---|
293 | )) |
---|
294 | |
---|
295 | schema['id'].widget.ignore_visible_ids=True |
---|
296 | schema['id'].widget.description="Should not contain spaces, underscores or mixed case. An 'X-Header-Set-Id' header with this id will be added." |
---|
297 | |
---|
298 | class 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 | |
---|
488 | registerType(HeaderSet, PROJECT_NAME) |
---|