[3296] | 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) |
---|