1 | """ |
---|
2 | CacheSetup |
---|
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
---|
4 | |
---|
5 | $Id: $ |
---|
6 | """ |
---|
7 | |
---|
8 | __authors__ = 'Geoff Davis <geoff@geoffdavis.net>' |
---|
9 | __docformat__ = 'restructuredtext' |
---|
10 | |
---|
11 | import sets |
---|
12 | import urlparse |
---|
13 | |
---|
14 | import zope.component |
---|
15 | from zope.interface import implements |
---|
16 | |
---|
17 | from Acquisition import aq_get |
---|
18 | from Acquisition import aq_inner |
---|
19 | from AccessControl import ClassSecurityInfo |
---|
20 | from AccessControl.PermissionRole import rolesForPermissionOn |
---|
21 | from BTrees import Length |
---|
22 | from DateTime import DateTime |
---|
23 | from ZODB.POSException import ConflictError |
---|
24 | from Products.Transience import Transience |
---|
25 | |
---|
26 | from Products.CMFCore.utils import UniqueObject |
---|
27 | from Products.CMFCore.utils import getToolByName |
---|
28 | from Products.CMFCore import permissions |
---|
29 | from Products.CMFCore.utils import _checkPermission |
---|
30 | |
---|
31 | from Products.Archetypes.atapi import Schema |
---|
32 | from Products.Archetypes.atapi import BaseSchema |
---|
33 | from Products.Archetypes.atapi import DisplayList |
---|
34 | from Products.Archetypes.atapi import OrderedBaseFolder |
---|
35 | from Products.Archetypes.atapi import registerType |
---|
36 | |
---|
37 | from Products.Archetypes.atapi import BooleanField |
---|
38 | from Products.Archetypes.atapi import StringField |
---|
39 | from Products.Archetypes.atapi import LinesField |
---|
40 | |
---|
41 | from Products.Archetypes.atapi import BooleanWidget |
---|
42 | from Products.Archetypes.atapi import SelectionWidget |
---|
43 | from Products.Archetypes.atapi import LinesWidget |
---|
44 | from Products.Archetypes.atapi import StringWidget |
---|
45 | |
---|
46 | from Products.Archetypes.debug import log_exc |
---|
47 | from Products.CMFPlone import PloneMessageFactory as _ |
---|
48 | |
---|
49 | from Products.statusmessages.interfaces import IStatusMessage |
---|
50 | |
---|
51 | from Products.CacheSetup.interfaces import ICacheTool |
---|
52 | from Products.CacheSetup.interfaces import IPurgeUrls |
---|
53 | from Products.CacheSetup.enabler import enableCacheFu |
---|
54 | from Products.CacheSetup import config |
---|
55 | from Products.CacheSetup import __version__ |
---|
56 | from nocatalog import NoCatalog |
---|
57 | |
---|
58 | schema = BaseSchema.copy() + Schema(( |
---|
59 | |
---|
60 | BooleanField( |
---|
61 | 'enabled', |
---|
62 | default=False, |
---|
63 | write_permission = permissions.ManagePortal, |
---|
64 | widget=BooleanWidget( |
---|
65 | label='Enable CacheFu', |
---|
66 | description='Uncheck to turn off CacheFu\'s caching behavior. Note: Disabling CacheFu ' |
---|
67 | 'does not purge proxy or browser caches so stale content may still continue ' |
---|
68 | 'to be served out of those caches.')), |
---|
69 | |
---|
70 | StringField( |
---|
71 | 'activePolicyId', |
---|
72 | default=config.DEFAULT_POLICY_ID, |
---|
73 | vocabulary='getActivePolicyVocabulary', |
---|
74 | write_permission = permissions.ManagePortal, |
---|
75 | widget=SelectionWidget( |
---|
76 | label='Active Cache Policy', |
---|
77 | description='Please indicate which cache policy to use.', |
---|
78 | condition='python:len(object.getActivePolicyVocabulary()) > 1')), |
---|
79 | |
---|
80 | # XXX: The cacheConfig field is no longer used, and kept here hidden only so that we can migrate away from it |
---|
81 | StringField( |
---|
82 | 'cacheConfig', |
---|
83 | widget=SelectionWidget( |
---|
84 | label='Cache Configuration', |
---|
85 | visible={'edit': 'invisible', 'view': 'invisible'})), |
---|
86 | |
---|
87 | StringField( |
---|
88 | 'proxyPurgeConfig', |
---|
89 | required=1, |
---|
90 | default='no-purge', |
---|
91 | write_permission = permissions.ManagePortal, |
---|
92 | vocabulary=DisplayList(( |
---|
93 | ('no-purge','No Purge (zope-only, or zope-behind-apache)'), |
---|
94 | ('no-rewrite','Simple Purge (squid/varnish in front)'), |
---|
95 | ('vhm-rewrite','Purge with VHM URLs (squid/varnish behind apache, VHM virtual hosting)'), |
---|
96 | ('custom-rewrite','Purge with custom URLs (squid/varnish behind apache, custom virtual hosting)'))), |
---|
97 | widget=SelectionWidget( |
---|
98 | label='Proxy Cache Purge Configuration', |
---|
99 | description='If you are using a caching proxy such as Squid or Varnish in front ' |
---|
100 | 'of Zope, CacheFu needs to be able to tell this proxy to purge its ' |
---|
101 | 'cache of certain pages. If Apache is in front of Squid/Varnish, then ' |
---|
102 | 'this depends on Apache\'s "virtual hosting" configuration. The most common ' |
---|
103 | 'Apache configuration generates VirtualHostMonster-style URLs with ' |
---|
104 | 'RewriteRules/ProxyPass. If you have a legacy CacheFu 1.0 Squid-Apache ' |
---|
105 | 'install or other custom Apache configuration, you may want to choose ' |
---|
106 | 'the "custom URLs" option and customize the rewritePurgeUrls.py script.')), |
---|
107 | |
---|
108 | LinesField( |
---|
109 | 'domains', |
---|
110 | edit_accessor='getDomains', |
---|
111 | write_permission = permissions.ManagePortal, |
---|
112 | widget=LinesWidget( |
---|
113 | label='Site Domains', |
---|
114 | description='Enter a list of domains for your site. This is not needed if you chose "No Purge" ' |
---|
115 | 'under the Proxy Cache Purge Configuration option above. ' |
---|
116 | 'If your site handles both http://www.mysite.com:80 and http://mysite.com:80, ' |
---|
117 | 'be sure to include both. Also include https versions of your domains if you use them. ' |
---|
118 | 'Be sure to include a port for each site.')), |
---|
119 | |
---|
120 | LinesField( |
---|
121 | 'squidURLs', |
---|
122 | edit_accessor='getSquidURLs', |
---|
123 | write_permission = permissions.ManagePortal, |
---|
124 | widget=LinesWidget( |
---|
125 | label='Proxy Cache Domains', |
---|
126 | description='Enter a list of domains for any purgeable proxy caches. This is not needed if ' |
---|
127 | 'you chose "No Purge" or "Simple Purge" under "Proxy Cache Purge Configuration" ' |
---|
128 | 'above. For example, if you are using Squid with Apache in front, there will ' |
---|
129 | 'commonly be a single squid instance at http://127.0.0.1:3128')), |
---|
130 | |
---|
131 | StringField( |
---|
132 | 'gzip', |
---|
133 | default='accept-encoding', |
---|
134 | write_permission = permissions.ManagePortal, |
---|
135 | vocabulary=DisplayList(( |
---|
136 | ('never','Never'), |
---|
137 | ('always','Always'), |
---|
138 | ('accept-encoding','Use Accept-Encoding header'), |
---|
139 | ('accept-encoding+user-agent','Use Accept-Encoding and User-Agent headers'))), |
---|
140 | widget=SelectionWidget( |
---|
141 | label='Compression', |
---|
142 | description='Should Zope compress pages before serving them, and if so, what criteria ' |
---|
143 | 'should be used to determine whether pages should be gzipped? The most common ' |
---|
144 | 'settings are "Never" (no compression) or "Use Accept-Encoding header" ' |
---|
145 | '(only compress content if the browser explicitly declared support for compression).')), |
---|
146 | |
---|
147 | StringField( |
---|
148 | 'varyHeader', |
---|
149 | default='Accept-Encoding', |
---|
150 | write_permission = permissions.ManagePortal, |
---|
151 | widget=StringWidget( |
---|
152 | label='Vary Header', |
---|
153 | size=60, |
---|
154 | description='Value for the Vary header. If you are using gzipping, you may need to include ' |
---|
155 | '"Accept-Encoding" and possibly "User-Agent". If you are running a multi-lingual site, ' |
---|
156 | 'you may also need "Accept-Language". Values should be separated by commas. ' |
---|
157 | '(Upon submit, this value will be cleaned up and checked for any obvious omissions)')), |
---|
158 | )) |
---|
159 | |
---|
160 | schema['title'].default = config.TOOL_TITLE |
---|
161 | schema['title'].widget.visible = {'edit': 'invisible', 'view': 'invisible'} |
---|
162 | schema['id'].widget.visible = {'edit': 'invisible', 'view': 'invisible'} |
---|
163 | |
---|
164 | class CacheTool(NoCatalog, OrderedBaseFolder, UniqueObject): |
---|
165 | """ |
---|
166 | """ |
---|
167 | |
---|
168 | implements(ICacheTool) |
---|
169 | archetype_name = 'Cache Configuration Tool' |
---|
170 | portal_type = meta_type = 'CacheTool' |
---|
171 | plone_tool = 1 |
---|
172 | security = ClassSecurityInfo() |
---|
173 | schema = schema |
---|
174 | content_icon = 'cachesetup_tool_icon.gif' |
---|
175 | global_allow = 0 |
---|
176 | allowed_content_types=('CachePolicy',) |
---|
177 | _catalog_count = None |
---|
178 | _permission_count = None |
---|
179 | _change_date = None |
---|
180 | |
---|
181 | actions = ( |
---|
182 | {'action': 'string:$object_url', |
---|
183 | 'category': 'object', |
---|
184 | 'id': 'view', |
---|
185 | 'name': 'Cache Setup', |
---|
186 | 'permissions': (permissions.ManagePortal,), |
---|
187 | 'visible': False |
---|
188 | }, |
---|
189 | ) |
---|
190 | |
---|
191 | aliases = { |
---|
192 | '(Default)': 'cache_tool_config', |
---|
193 | 'view' : 'cache_tool_config', |
---|
194 | 'edit' : 'base_edit' |
---|
195 | } |
---|
196 | |
---|
197 | def __init__(self, oid, **kwargs): |
---|
198 | # keep track of the installed version |
---|
199 | OrderedBaseFolder.__init__(self, oid, **kwargs) |
---|
200 | self.installedversion = __version__ |
---|
201 | |
---|
202 | def updateInstalledVersion(self): |
---|
203 | # call this method after a reinstall |
---|
204 | # to update installedversion tracking |
---|
205 | self.installedversion = __version__ |
---|
206 | |
---|
207 | def initializeArchetype(self, **kwargs): |
---|
208 | # get values from other places before archetypes initializes them |
---|
209 | squid_urls = self._getSquidUrls() |
---|
210 | OrderedBaseFolder.initializeArchetype(self, **kwargs) |
---|
211 | # don't stomp on squid urls |
---|
212 | self._setSquidUrls(squid_urls) |
---|
213 | |
---|
214 | def getActivePolicyVocabulary(self, display=None): |
---|
215 | active = self.getActivePolicyId() |
---|
216 | vocabulary = [(p.getId(), p.Title()) for p in self.objectValues() if p.portal_type=="CachePolicy"] |
---|
217 | if display: |
---|
218 | vocabulary = [('(active policy)', '(active policy)')] + vocabulary |
---|
219 | return DisplayList(tuple(vocabulary)) |
---|
220 | |
---|
221 | # if active is gone, find another |
---|
222 | def getActivePolicyId(self): |
---|
223 | policy_id = self.getField('activePolicyId').get(self) |
---|
224 | if getattr(self, str(policy_id), None) is None: |
---|
225 | rules = getattr(self, config.RULES_ID, None) |
---|
226 | if rules is not None: |
---|
227 | # we haven't migrated yet |
---|
228 | return '' |
---|
229 | else: |
---|
230 | # policy deleted so try the first one |
---|
231 | policies = self.objectValues() |
---|
232 | if len(policies): |
---|
233 | policy_id = policies[0].getId() |
---|
234 | else: |
---|
235 | # no policies so add a blank set |
---|
236 | self.addPolicyFolder(config.DEFAULT_POLICY_ID, 'Default Cache Policy') |
---|
237 | policy_id = config.DEFAULT_POLICY_ID |
---|
238 | self.setActivePolicyId(policy_id) |
---|
239 | return policy_id |
---|
240 | |
---|
241 | # use browser cookies to store current display policy id |
---|
242 | def setDisplayPolicy(self, id=None, camefrom=None, redirect=True): |
---|
243 | request = self.REQUEST |
---|
244 | response = request.response |
---|
245 | cookie_name = 'cachetool_policy' |
---|
246 | id = str(id) |
---|
247 | display_policy = getattr(self, id, None) |
---|
248 | if getattr(self, id, None) is None: |
---|
249 | # clear the cookie |
---|
250 | response.setCookie(cookie_name, '', path='/') |
---|
251 | id = None |
---|
252 | else: |
---|
253 | # expire cookie after 6 hours |
---|
254 | expires = (DateTime() + .25).toZone('GMT').rfc822() |
---|
255 | response.setCookie(cookie_name, id, path='/', expires=expires) |
---|
256 | if redirect: |
---|
257 | if camefrom is None: |
---|
258 | response.redirect('%s/cache_policy_config' % self.absolute_url()) |
---|
259 | else: |
---|
260 | policy = self.getPolicy(id) |
---|
261 | response.redirect('%s/%s' % (policy.absolute_url(), camefrom)) |
---|
262 | return '' |
---|
263 | |
---|
264 | def getEnabled(self): |
---|
265 | """Let's disable CacheFu if the file system version doesn't |
---|
266 | match the installed version to make sure we don't kill the site |
---|
267 | if the schemas have changed. |
---|
268 | """ |
---|
269 | if getattr(self, 'installedversion', None) != __version__ : |
---|
270 | return False |
---|
271 | return self.getField('enabled').get(self) |
---|
272 | |
---|
273 | def setEnabled(self, value): |
---|
274 | # convert to boolean for backwards compat with Plone 2.5 |
---|
275 | if not value or value == '0' or value == 'False': |
---|
276 | value = False |
---|
277 | else: |
---|
278 | value = True |
---|
279 | # if version mismatch, send status message |
---|
280 | if value and getattr(self, 'installedversion', None) != __version__ : |
---|
281 | IStatusMessage(self.REQUEST).addStatusMessage( |
---|
282 | _(u"Refusing to enable CacheSetup until installed version matches " |
---|
283 | u"filesystem version. Update/reinstall CacheSetup to fix."), |
---|
284 | type="error") |
---|
285 | # change field only if different |
---|
286 | elif value != self.getField('enabled').get(self): |
---|
287 | self.getField('enabled').set(self, value) |
---|
288 | enableCacheFu(self, value) |
---|
289 | |
---|
290 | def getPolicy(self, policy_id=None): |
---|
291 | if policy_id is None: |
---|
292 | policy_id = self.getActivePolicyId() |
---|
293 | policy = getattr(self, policy_id, None) |
---|
294 | if policy is None: |
---|
295 | rules = getattr(self, config.RULES_ID, None) |
---|
296 | if rules is not None: |
---|
297 | # we haven't migrated yet |
---|
298 | policy = self |
---|
299 | else: |
---|
300 | # policy deleted so try the first one |
---|
301 | policies = self.objectValues() |
---|
302 | if len(policies): |
---|
303 | policy = policies[0] |
---|
304 | else: |
---|
305 | # no policies so add a blank set |
---|
306 | self.addPolicyFolder(config.DEFAULT_POLICY_ID, 'Default Cache Policy') |
---|
307 | policy = getattr(self, config.DEFAULT_POLICY_ID) |
---|
308 | return policy |
---|
309 | |
---|
310 | def getDisplayPolicy(self): |
---|
311 | request = self.REQUEST |
---|
312 | response = request.response |
---|
313 | cookie_name = 'cachetool_policy' |
---|
314 | id = request.cookies.get(cookie_name, '') |
---|
315 | display_policy = getattr(self, id, None) |
---|
316 | if display_policy is None: |
---|
317 | if id != '': response.setCookie(cookie_name, '', path='/') |
---|
318 | display_policy = self.getPolicy() |
---|
319 | return display_policy |
---|
320 | |
---|
321 | def getRules(self, policy_id=None): |
---|
322 | policy = self.getPolicy(policy_id) |
---|
323 | rules = getattr(policy, config.RULES_ID, None) |
---|
324 | if rules is None: |
---|
325 | policy_id = policy.getId() |
---|
326 | self.addRulesFolder(policy_id) |
---|
327 | rules = self.getRules(policy_id) |
---|
328 | return rules |
---|
329 | |
---|
330 | def getHeaderSets(self, policy_id=None): |
---|
331 | policy = self.getPolicy(policy_id) |
---|
332 | header_sets = getattr(policy, config.HEADERSETS_ID, None) |
---|
333 | if header_sets is None: |
---|
334 | policy_id = policy.getId() |
---|
335 | self.addHeaderSetsFolder(policy_id) |
---|
336 | header_sets = self.getHeaderSets(policy_id) |
---|
337 | return header_sets |
---|
338 | |
---|
339 | def getHeaderSetById(self, id): |
---|
340 | return getattr(self.getHeaderSets(), id) |
---|
341 | |
---|
342 | def addPolicyFolder(self, policy_id, policy_title, empty=None): |
---|
343 | policy = getattr(self, policy_id, None) |
---|
344 | if policy is not None: |
---|
345 | self.manage_delObjects(policy_id) |
---|
346 | self.invokeFactory(id=policy_id, type_name='CachePolicy') |
---|
347 | policy = getattr(self, policy_id) |
---|
348 | policy.unmarkCreationFlag() |
---|
349 | policy.setTitle(policy_title) |
---|
350 | policy.reindexObject() |
---|
351 | if empty is not None: |
---|
352 | policy_id = policy.getId() |
---|
353 | self.addRulesFolder(policy_id) |
---|
354 | self.addHeaderSetsFolder(policy_id) |
---|
355 | |
---|
356 | def addRulesFolder(self, policy_id): |
---|
357 | policy = getattr(self, policy_id) |
---|
358 | policy.allowed_content_types = ('RuleFolder',) |
---|
359 | policy.invokeFactory(id=config.RULES_ID, type_name='RuleFolder') |
---|
360 | policy.allowed_content_types = () |
---|
361 | rules = self.getRules(policy.getId()) |
---|
362 | rules.unmarkCreationFlag() |
---|
363 | rules.setTitle('Rules') |
---|
364 | rules.reindexObject() |
---|
365 | |
---|
366 | def addHeaderSetsFolder(self, policy_id): |
---|
367 | policy = getattr(self, policy_id) |
---|
368 | policy.allowed_content_types = ('HeaderSetFolder',) |
---|
369 | policy.invokeFactory(id=config.HEADERSETS_ID, type_name='HeaderSetFolder') |
---|
370 | policy.allowed_content_types = () |
---|
371 | header_sets = self.getHeaderSets(policy.getId()) |
---|
372 | header_sets.unmarkCreationFlag() |
---|
373 | header_sets.setTitle('Headers') |
---|
374 | header_sets.reindexObject() |
---|
375 | |
---|
376 | # ##### Counters for use in ETag/cache key building ##### |
---|
377 | |
---|
378 | def updateChangeDate(self): |
---|
379 | # change_date is a DateTime that we will update |
---|
380 | # every time we increment a counter |
---|
381 | if self._change_date is None: |
---|
382 | self._change_date = Transience.Increaser(DateTime()) |
---|
383 | self._change_date.set(DateTime()) |
---|
384 | |
---|
385 | def getChangeDate(self): |
---|
386 | return self._change_date() |
---|
387 | |
---|
388 | def incrementCatalogCount(self): |
---|
389 | # catalog_count is a minimal counter object that we will increment |
---|
390 | # every time an object is indexed/reindexed/unindexed -- we will |
---|
391 | # then use this for cache invalidation |
---|
392 | if self._catalog_count is None: |
---|
393 | self._catalog_count = Length.Length() |
---|
394 | self._catalog_count.change(1) |
---|
395 | self.updateChangeDate() |
---|
396 | |
---|
397 | def getCatalogCount(self): |
---|
398 | return self._catalog_count() |
---|
399 | |
---|
400 | def incrementPermissionCount(self): |
---|
401 | # permission_count is a minimal counter object that we will increment every |
---|
402 | # time the relationship between roles and permissions changes. We will use |
---|
403 | # this value for cache invalidation |
---|
404 | if self._permission_count is None: |
---|
405 | self._permission_count = Length.Length() |
---|
406 | self._permission_count.change(1) |
---|
407 | self.updateChangeDate() |
---|
408 | |
---|
409 | def getPermissionCount(self): |
---|
410 | if self._permission_count is None: |
---|
411 | self._permission_count = Length.Length() |
---|
412 | return self._permission_count() |
---|
413 | |
---|
414 | # ##### Accessors, mutators, and helper methods used in configuration ###### |
---|
415 | |
---|
416 | def _getCompleteUrl(self, url): |
---|
417 | if url.find('//') == -1: |
---|
418 | url = 'http://' + url |
---|
419 | p = urlparse.urlparse(url) |
---|
420 | protocol = p[0] |
---|
421 | if not protocol: |
---|
422 | protocol = 'http' |
---|
423 | host = p[1] |
---|
424 | split_host = host.split(':') |
---|
425 | if len(split_host) == 1: |
---|
426 | if protocol == 'https': |
---|
427 | port = '443' |
---|
428 | else: |
---|
429 | port = '80' |
---|
430 | host = split_host[0] + ':' + port |
---|
431 | return urlparse.urlunparse((protocol, host, '','','','')) |
---|
432 | |
---|
433 | def _getSquidUrls(self): |
---|
434 | # get a list of urls from squid |
---|
435 | squid_tool = getToolByName(self, 'portal_squid') |
---|
436 | return tuple([url for url in squid_tool.getSquidURLs().split('\n') if url]) |
---|
437 | |
---|
438 | def _setSquidUrls(self, list_of_urls): |
---|
439 | # pass a \n-joined list of urls to squid tool |
---|
440 | squid_tool = getToolByName(self, 'portal_squid') |
---|
441 | squid_tool.manage_setSquidSettings('\n'.join(list_of_urls)) |
---|
442 | |
---|
443 | def hasPurgeableProxy(self): |
---|
444 | # return self.getCacheConfig() in ('squid', 'squid_behind_apache') |
---|
445 | return self.getProxyPurgeConfig() != 'no-purge' |
---|
446 | |
---|
447 | def getDomains(self): |
---|
448 | if self.getProxyPurgeConfig() in ('vhm-rewrite','custom-rewrite'): |
---|
449 | return self.getField('domains').get(self) |
---|
450 | else: |
---|
451 | return self._getSquidUrls() |
---|
452 | |
---|
453 | security.declareProtected(permissions.ManagePortal, 'setDomains') |
---|
454 | def setDomains(self, value): |
---|
455 | if value is None: |
---|
456 | value = '' |
---|
457 | if type(value) == type(''): |
---|
458 | value = value.replace('\r','\n') |
---|
459 | value = value.split('\n') |
---|
460 | value = [v.strip() for v in value if v] |
---|
461 | domains = [] |
---|
462 | for v in value: |
---|
463 | domains.append(self._getCompleteUrl(v)) |
---|
464 | if self.getProxyPurgeConfig() in ('vhm-rewrite','custom-rewrite'): |
---|
465 | self.getField('domains').set(self, domains) |
---|
466 | else: |
---|
467 | self._setSquidUrls(domains) |
---|
468 | |
---|
469 | security.declareProtected(permissions.View, 'getSquidURLs') |
---|
470 | def getSquidURLs(self): |
---|
471 | if self.getProxyPurgeConfig() in ('vhm-rewrite','custom-rewrite'): |
---|
472 | return self._getSquidUrls() |
---|
473 | else: |
---|
474 | return '' |
---|
475 | |
---|
476 | security.declareProtected(permissions.ManagePortal, 'setSquidURLs') |
---|
477 | def setSquidURLs(self, value): |
---|
478 | if self.getProxyPurgeConfig() in ('vhm-rewrite','custom-rewrite'): |
---|
479 | if value is None: |
---|
480 | value = '' |
---|
481 | if type(value) == type(''): |
---|
482 | value = value.replace('\r','\n') |
---|
483 | value = value.split('\n') |
---|
484 | self._setSquidUrls([self._getCompleteUrl(v) for v in value if v]) |
---|
485 | |
---|
486 | security.declareProtected(permissions.ManagePortal, 'setVaryHeader') |
---|
487 | def setVaryHeader(self, value): |
---|
488 | # Correct for missing headers |
---|
489 | values = [v.strip() for v in value.split(',')] |
---|
490 | request = aq_get(self, 'REQUEST', None) |
---|
491 | if request is not None: |
---|
492 | gzip = request.get('gzip',None) |
---|
493 | else: |
---|
494 | gzip = self.getGzip() |
---|
495 | if gzip in ('accept-encoding', 'accept-encoding+user-agent'): |
---|
496 | if not 'Accept-Encoding' in values: |
---|
497 | values.append('Accept-Encoding') |
---|
498 | if gzip == 'accept-encoding+user-agent': |
---|
499 | if not 'User-Agent' in values: |
---|
500 | values.append('User-Agent') |
---|
501 | value = ', '.join(values) |
---|
502 | self.getField('varyHeader').set(self, value) |
---|
503 | |
---|
504 | def post_validate(self, REQUEST, errors): |
---|
505 | proxy_purge_config = REQUEST.get('proxyPurgeConfig',None) |
---|
506 | squid_urls = REQUEST.get('squidURLs',None) |
---|
507 | if proxy_purge_config in ('vhm-rewrite','custom-rewrite'): |
---|
508 | if not squid_urls: |
---|
509 | errors['squidURLs'] = 'Please enter the URLs for your proxy caches. We need this to generate the URLs for PURGE requests.' |
---|
510 | else: |
---|
511 | if squid_urls: |
---|
512 | errors['squidURLs'] = 'Set this field only if rewriting proxy cache PURGE requests' |
---|
513 | |
---|
514 | if proxy_purge_config in ('no-rewrite','vhm-rewrite','custom-rewrite'): |
---|
515 | if not REQUEST.get('domains', None): |
---|
516 | errors['domains'] = 'Please enter the domains that you will be caching. We need this to generate proper PURGE requests.' |
---|
517 | |
---|
518 | # Not needed anymore since setVaryHeader fixes itself if necessary |
---|
519 | #gzip = REQUEST.get('gzip',None) |
---|
520 | #vary_header = REQUEST.get('varyHeader','') |
---|
521 | #values = [v.strip() for v in vary_header.split(',')] |
---|
522 | #if gzip in ('accept-encoding', 'accept-encoding+user-agent'): |
---|
523 | # if not 'Accept-Encoding' in values: |
---|
524 | # errors['varyHeader'] = 'When Compression is set to "%s", you need "Accept-Encoding" in the Vary header' % gzip |
---|
525 | # if gzip == 'accept-encoding+user-agent': |
---|
526 | # if not 'User-Agent' in values: |
---|
527 | # errors['varyHeader'] = 'When Compression is set to %s, you need "User-Agent" in the Vary header' % gzip |
---|
528 | |
---|
529 | security.declareProtected(permissions.ManagePortal, 'manage_purgePageCache') |
---|
530 | def manage_purgePageCache(self, REQUEST=None): |
---|
531 | """Purge the page cache manager""" |
---|
532 | msg = 'portal_status_message=Page+cache+not+purged:+CacheFu+disabled' |
---|
533 | if self.getEnabled(): |
---|
534 | pc = getToolByName(self, config.PAGE_CACHE_MANAGER_ID) |
---|
535 | pc.manage_purge() |
---|
536 | msg = 'portal_status_message=Page+cache+purged' |
---|
537 | if REQUEST is not None: |
---|
538 | url = REQUEST.get('HTTP_REFERER', self.absolute_url()+'/edit') |
---|
539 | if url.find('?') != -1: |
---|
540 | url += '&' + msg |
---|
541 | else: |
---|
542 | url += '?' + msg |
---|
543 | return REQUEST.RESPONSE.redirect(url) |
---|
544 | |
---|
545 | # ##### Helper methods used for building ETags and for header setting ###### |
---|
546 | |
---|
547 | def canAnonymousView(self, object): |
---|
548 | """Returns True if anonymous users can view an object""" |
---|
549 | if 'Anonymous' in rolesForPermissionOn('View', object): |
---|
550 | return True |
---|
551 | # XXX i am not sure it is possible to assign local roles to the anonymous user |
---|
552 | # XXX if it is, there may need to be some local role tomfoolery here |
---|
553 | # XXX something like the following |
---|
554 | # roles_with_view = {} |
---|
555 | # for r in rolesForPermissionOn('View', obj): |
---|
556 | # roles_with_view[r] = 1 |
---|
557 | # try: |
---|
558 | # all_local_roles = portal.acl_users._getAllLocalRoles(obj) |
---|
559 | # except AttributeError: |
---|
560 | # all_local_roles = _mergedLocalRoles(obj) |
---|
561 | # if 'Anonymous user' in all_local_roles: |
---|
562 | # for r in all_local_roles['Anonymous user']: |
---|
563 | # if r in roles_with_view: |
---|
564 | # return True |
---|
565 | return False |
---|
566 | |
---|
567 | security.declarePublic('isGzippable') |
---|
568 | def isGzippable(self, css=0, js=0, REQUEST=None): |
---|
569 | """Indicate whether gzipping is allowed for the current request. Returns |
---|
570 | a tuple. The first argument indicates whether gzipping should be enabled, |
---|
571 | the second indicates whether gzipping should be forced, and the third |
---|
572 | whether the browser will accept gzipped content.""" |
---|
573 | # force: force http compression even if the browser doesn't send an accept |
---|
574 | # debug: return compression state (0: no, 1: yes, 2: force) |
---|
575 | # css: set this to 1 inside a css file (for later use) |
---|
576 | # js: set this to 1 inside a js file (for later use) |
---|
577 | |
---|
578 | if REQUEST is None: |
---|
579 | REQUEST = self.REQUEST |
---|
580 | use_gzip = self.getGzip() |
---|
581 | if not self.getEnabled(): |
---|
582 | use_gzip = 'never' |
---|
583 | |
---|
584 | force = 0 |
---|
585 | if use_gzip == 'never': |
---|
586 | enable_compression = 0 |
---|
587 | elif use_gzip == 'always': |
---|
588 | enable_compression = 1 |
---|
589 | force = 1 |
---|
590 | elif use_gzip == 'accept-encoding': |
---|
591 | # compress everything except css and js |
---|
592 | enable_compression = 1 |
---|
593 | elif use_gzip == 'accept-encoding+user-agent': |
---|
594 | # gzip compatibility info courtesy of |
---|
595 | # http://httpd.apache.org/docs/2.2/mod/mod_deflate.html |
---|
596 | user_agent = REQUEST.get('HTTP_USER_AGENT', '') |
---|
597 | if user_agent.startswith('Mozilla/4'): |
---|
598 | # Netscape 4.x can't handle gzipped css and js |
---|
599 | enable_compression = (css==0 and js==0) |
---|
600 | # Netscape 4.0.6-4.0.8 has some gzip-related bugs |
---|
601 | if user_agent[len('Mozilla/4.')] in ('6','7','8'): |
---|
602 | enable_compression = 0 |
---|
603 | # Some versions of MSIE pretend to be Netscape 4.x but are OK with gzipping |
---|
604 | if user_agent.find('MSIE'): |
---|
605 | enable_compression = 1 |
---|
606 | |
---|
607 | return (enable_compression, force, REQUEST.get('HTTP_ACCEPT_ENCODING', '').find('gzip') != -1) |
---|
608 | |
---|
609 | # ##### Main methods ###### |
---|
610 | |
---|
611 | security.declarePublic('getRuleAndHeaderSet') |
---|
612 | def getRuleAndHeaderSet(self, request, object, view, member): |
---|
613 | """Get the caching rule that applies here and the header set specified by the rule""" |
---|
614 | if not self.getEnabled(): |
---|
615 | return (None, None) |
---|
616 | rules = self.getRules().objectValues() |
---|
617 | for rule in rules: |
---|
618 | try: |
---|
619 | header_set = rule.getHeaderSet(request, object, view, member) |
---|
620 | if header_set is not None: |
---|
621 | return (rule, header_set) |
---|
622 | except ConflictError: |
---|
623 | raise |
---|
624 | except: |
---|
625 | log_exc() |
---|
626 | return (None, None) |
---|
627 | |
---|
628 | security.declarePublic('getUrlsToPurge') |
---|
629 | def getUrlsToPurge(self, object): |
---|
630 | """Get a list of URLs to be purged when the given object is added / modified / deleted""" |
---|
631 | |
---|
632 | # if nothing to purge or cachefu disabled, return an empty list |
---|
633 | if not self.hasPurgeableProxy() or not self.getEnabled(): |
---|
634 | return [] |
---|
635 | |
---|
636 | relative_urls = sets.Set() |
---|
637 | rules = self.getRules().objectValues() |
---|
638 | for rule in rules: |
---|
639 | try: |
---|
640 | rule.getRelativeUrlsToPurge(object, relative_urls) |
---|
641 | except ConflictError: |
---|
642 | raise |
---|
643 | except: |
---|
644 | log_exc() |
---|
645 | |
---|
646 | for adapter in zope.component.subscribers([aq_inner(object)], IPurgeUrls): |
---|
647 | relative_urls.union_update(adapter.getRelativeUrls()) |
---|
648 | |
---|
649 | relative_urls = list(relative_urls) |
---|
650 | |
---|
651 | absolute_urls=[] |
---|
652 | for adapter in zope.component.subscribers([aq_inner(object)], IPurgeUrls): |
---|
653 | absolute_urls.extend(adapter.getAbsoluteUrls(relative_urls)) |
---|
654 | |
---|
655 | if relative_urls: |
---|
656 | proxy_purge_config = self.getProxyPurgeConfig() |
---|
657 | if proxy_purge_config == 'vhm-rewrite': |
---|
658 | # unless defined otherwise, we assume urls passed to the cache proxy |
---|
659 | # are of the standard form expected by the VirtualHostMonster: |
---|
660 | # [squid_url]/VirtualHostBase/[protocol]/[host]:[port]/[path to portal root]/VirtualHostRoot/[path] |
---|
661 | url_tool = getToolByName(self, 'portal_url') |
---|
662 | portal_path = '/'.join(url_tool.getPortalObject().getPhysicalPath()) |
---|
663 | domains = self.getDomains() |
---|
664 | prefixes = [] |
---|
665 | for d in domains: |
---|
666 | p = urlparse.urlparse(d) |
---|
667 | protocol = p[0] |
---|
668 | host = p[1] |
---|
669 | split_host = host.split(':') |
---|
670 | host = split_host[0] |
---|
671 | port = split_host[1] |
---|
672 | prefixes.append('VirtualHostBase/%s/%s:%s%s/VirtualHostRoot/' \ |
---|
673 | % (protocol, host, port, portal_path)) |
---|
674 | relative_urls = [prefix+url for prefix in prefixes for url in relative_urls] |
---|
675 | elif proxy_purge_config == 'custom-rewrite': |
---|
676 | domains = [urlparse.urlparse(d) for d in self.getDomains()] |
---|
677 | relative_urls = self.rewritePurgeUrls(relative_urls, domains) |
---|
678 | |
---|
679 | |
---|
680 | return relative_urls + absolute_urls |
---|
681 | |
---|
682 | # A few helper methods |
---|
683 | |
---|
684 | def getMember(self): |
---|
685 | """Utility method for getting a member for use in expression contexts. Returns |
---|
686 | the Member object for the currently authenticated member or None if the |
---|
687 | user is not authenticated.""" |
---|
688 | pm = getToolByName(self, 'portal_membership', None) |
---|
689 | # stick to the CachingPolicyManager expression convention |
---|
690 | if not pm or pm.isAnonymousUser(): |
---|
691 | return None |
---|
692 | else: |
---|
693 | return pm.getAuthenticatedMember() |
---|
694 | |
---|
695 | # a few methods for generating non-hideous default ids |
---|
696 | security.declareProtected(permissions.ManagePortal, 'generateUniqueId') |
---|
697 | def generateUniqueId(self, type_name): |
---|
698 | context = self.REQUEST.PARENTS[0] |
---|
699 | question_ids = context.objectIds() |
---|
700 | n = len(question_ids)+1 |
---|
701 | while str(n) in question_ids: |
---|
702 | n = n + 1 |
---|
703 | return str(n) |
---|
704 | |
---|
705 | def _isIDAutoGenerated(self, id): |
---|
706 | try: |
---|
707 | int(id) |
---|
708 | return True |
---|
709 | except: |
---|
710 | return False |
---|
711 | |
---|
712 | registerType(CacheTool, config.PROJECT_NAME) |
---|