Source code for cubicweb.web.httpcache

# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://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 <https://www.gnu.org/licenses/>.
"""HTTP cache managers"""


from calendar import timegm
from datetime import datetime

from cubicweb import view as viewmod


[docs]class NoHTTPCacheManager: """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().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().etag() if len(self.cw_rset) > 1: raise NoEtag() etag = super().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
[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), ]