Source code for cubicweb.web.httpcache

# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""HTTP cache managers"""

__docformat__ = "restructuredtext en"

from calendar import timegm
from datetime import datetime

[docs]class NoHTTPCacheManager(object): """default cache manager: set no-cache cache control policy""" def __init__(self, view): self.view = view self.req = view._cw self.cw_rset = view.cw_rset def set_headers(self): self.req.set_header('Cache-control', 'no-cache') self.req.set_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
[docs]class MaxAgeHTTPCacheManager(NoHTTPCacheManager): """max-age cache manager: set max-age cache control policy, with max-age specified with the `cache_max_age` attribute of the view """ def set_headers(self): self.req.set_header('Cache-control', 'max-age=%s' % self.view.cache_max_age)
[docs]class EtagHTTPCacheManager(NoHTTPCacheManager): """etag based cache manager for startup views * etag is generated using the view name and the user's groups * set policy to 'must-revalidate' and expires to the current time to force revalidation on each request """ def etag(self): if not self.req.cnx: # session without established connection to the repo return self.view.__regid__ return self.view.__regid__ + '/' + ','.join(sorted(self.req.user.groups)) def max_age(self): # 0 to actually force revalidation return 0 def last_modified(self): """return view's last modified GMT time""" return self.view.last_modified() def set_headers(self): req = self.req try: req.set_header('Etag', 'W/"%s"' % self.etag()) except NoEtag: super(EtagHTTPCacheManager, self).set_headers() return req.set_header('Cache-control', 'must-revalidate,max-age=%s' % self.max_age()) mdate = self.last_modified() # use a timestamp, not a formatted raw header, and let # the front-end correctly generate it # ("%a, %d %b %Y %H:%M:%S GMT" return localized date that # twisted don't parse correctly) req.set_header('Last-modified', timegm(mdate.timetuple()), raw=False)
[docs]class EntityHTTPCacheManager(EtagHTTPCacheManager): """etag based cache manager for view displaying a single entity * etag is generated using entity's eid, the view name and the user's groups * get last modified time from the entity definition (this may not be the entity's modification time since a view may include some related entities with a modification time to consider) using the `last_modified` method """ def etag(self): if self.cw_rset is None or len(self.cw_rset) == 0: # entity startup view for instance return super(EntityHTTPCacheManager, self).etag() if len(self.cw_rset) > 1: raise NoEtag() etag = super(EntityHTTPCacheManager, self).etag() eid = self.cw_rset[0][0] if self.req.user.owns(eid): etag += ',owners' return str(eid) + '/' + etag
[docs]class NoEtag(Exception): """an etag can't be generated"""
__all__ = ('NoHTTPCacheManager', 'MaxAgeHTTPCacheManager', 'EtagHTTPCacheManager', 'EntityHTTPCacheManager') # monkey patching, so view doesn't depends on this module and we have all # http cache related logic here from cubicweb import view as viewmod
[docs]def set_http_cache_headers(self): self.http_cache_manager(self).set_headers()
viewmod.View.set_http_cache_headers = set_http_cache_headers def last_modified(self): """return the date/time where this view should be considered as modified. Take care of possible related objects modifications. /!\ must return GMT time /!\ """ # XXX check view module's file modification time in dev mod ? ctime = datetime.utcnow() if self.cache_max_age: mtime = self._cw.header_if_modified_since() if mtime: tdelta = (ctime - mtime) if tdelta.days * 24*60*60 + tdelta.seconds <= self.cache_max_age: return mtime # mtime = ctime will force page rerendering return ctime viewmod.View.last_modified = last_modified # configure default caching viewmod.View.http_cache_manager = NoHTTPCacheManager # max-age=0 to actually force revalidation when needed viewmod.View.cache_max_age = 0 viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default ### HTTP Cache validator ############################################ def get_validators(headers_in): """return a list of http condition validator relevant to this request """ result = [] for header, func in VALIDATORS: value = headers_in.getHeader(header) if value is not None: result.append((func, value)) return result def if_modified_since(ref_date, headers_out): last_modified = headers_out.getHeader('last-modified') if last_modified is None: return True return ref_date < last_modified def if_none_match(tags, headers_out): etag = headers_out.getHeader('etag') if etag is None: return True return not ((etag in tags) or ('*' in tags)) VALIDATORS = [ ('if-modified-since', if_modified_since), #('if-unmodified-since', if_unmodified_since), ('if-none-match', if_none_match), #('if-modified-since', if_modified_since), ]