web/httpcache.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """HTTP cache managers"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 
       
    22 from time import mktime
       
    23 from datetime import datetime
       
    24 
       
    25 class NoHTTPCacheManager(object):
       
    26     """default cache manager: set no-cache cache control policy"""
       
    27     def __init__(self, view):
       
    28         self.view = view
       
    29         self.req = view._cw
       
    30         self.cw_rset = view.cw_rset
       
    31 
       
    32     def set_headers(self):
       
    33         self.req.set_header('Cache-control', 'no-cache')
       
    34         self.req.set_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
       
    35 
       
    36 
       
    37 class MaxAgeHTTPCacheManager(NoHTTPCacheManager):
       
    38     """max-age cache manager: set max-age cache control policy, with max-age
       
    39     specified with the `cache_max_age` attribute of the view
       
    40     """
       
    41     def set_headers(self):
       
    42         self.req.set_header('Cache-control',
       
    43                             'max-age=%s' % self.view.cache_max_age)
       
    44 
       
    45 
       
    46 class EtagHTTPCacheManager(NoHTTPCacheManager):
       
    47     """etag based cache manager for startup views
       
    48 
       
    49     * etag is generated using the view name and the user's groups
       
    50     * set policy to 'must-revalidate' and expires to the current time to force
       
    51       revalidation on each request
       
    52     """
       
    53 
       
    54     def etag(self):
       
    55         if not self.req.cnx: # session without established connection to the repo
       
    56             return self.view.__regid__
       
    57         return self.view.__regid__ + '/' + ','.join(sorted(self.req.user.groups))
       
    58 
       
    59     def max_age(self):
       
    60         # 0 to actually force revalidation
       
    61         return 0
       
    62 
       
    63     def last_modified(self):
       
    64         """return view's last modified GMT time"""
       
    65         return self.view.last_modified()
       
    66 
       
    67     def set_headers(self):
       
    68         req = self.req
       
    69         try:
       
    70             req.set_header('Etag', '"%s"' % self.etag())
       
    71         except NoEtag:
       
    72             super(EtagHTTPCacheManager, self).set_headers()
       
    73             return
       
    74         req.set_header('Cache-control',
       
    75                        'must-revalidate,max-age=%s' % self.max_age())
       
    76         mdate = self.last_modified()
       
    77         # use a timestamp, not a formatted raw header, and let
       
    78         # the front-end correctly generate it
       
    79         # ("%a, %d %b %Y %H:%M:%S GMT" return localized date that
       
    80         # twisted don't parse correctly)
       
    81         req.set_header('Last-modified', mktime(mdate.timetuple()), raw=False)
       
    82 
       
    83 
       
    84 class EntityHTTPCacheManager(EtagHTTPCacheManager):
       
    85     """etag based cache manager for view displaying a single entity
       
    86 
       
    87     * etag is generated using entity's eid, the view name and the user's groups
       
    88     * get last modified time from the entity definition (this may not be the
       
    89       entity's modification time since a view may include some related entities
       
    90       with a modification time to consider) using the `last_modified` method
       
    91     """
       
    92     def etag(self):
       
    93         if self.cw_rset is None or len(self.cw_rset) == 0: # entity startup view for instance
       
    94             return super(EntityHTTPCacheManager, self).etag()
       
    95         if len(self.cw_rset) > 1:
       
    96             raise NoEtag()
       
    97         etag = super(EntityHTTPCacheManager, self).etag()
       
    98         eid = self.cw_rset[0][0]
       
    99         if self.req.user.owns(eid):
       
   100             etag += ',owners'
       
   101         return str(eid) + '/' + etag
       
   102 
       
   103 
       
   104 class NoEtag(Exception):
       
   105     """an etag can't be generated"""
       
   106 
       
   107 __all__ = ('GMTOFFSET',
       
   108            'NoHTTPCacheManager', 'MaxAgeHTTPCacheManager',
       
   109            'EtagHTTPCacheManager', 'EntityHTTPCacheManager')
       
   110 
       
   111 # monkey patching, so view doesn't depends on this module and we have all
       
   112 # http cache related logic here
       
   113 
       
   114 from cubicweb import view as viewmod
       
   115 
       
   116 def set_http_cache_headers(self):
       
   117     self.http_cache_manager(self).set_headers()
       
   118 viewmod.View.set_http_cache_headers = set_http_cache_headers
       
   119 
       
   120 
       
   121 def last_modified(self):
       
   122     """return the date/time where this view should be considered as
       
   123     modified. Take care of possible related objects modifications.
       
   124 
       
   125     /!\ must return GMT time /!\
       
   126     """
       
   127     # XXX check view module's file modification time in dev mod ?
       
   128     ctime = datetime.utcnow()
       
   129     if self.cache_max_age:
       
   130         mtime = self._cw.header_if_modified_since()
       
   131         if mtime:
       
   132             tdelta = (ctime - mtime)
       
   133             if tdelta.days * 24*60*60 + tdelta.seconds <= self.cache_max_age:
       
   134                 return mtime
       
   135     # mtime = ctime will force page rerendering
       
   136     return ctime
       
   137 viewmod.View.last_modified = last_modified
       
   138 
       
   139 
       
   140 # configure default caching
       
   141 viewmod.View.http_cache_manager = NoHTTPCacheManager
       
   142 # max-age=0 to actually force revalidation when needed
       
   143 viewmod.View.cache_max_age = 0
       
   144 
       
   145 viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
       
   146 viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
       
   147 
       
   148 
       
   149 ### HTTP Cache validator ############################################
       
   150 
       
   151 
       
   152 
       
   153 def get_validators(headers_in):
       
   154     """return a list of http condition validator relevant to this request
       
   155     """
       
   156     result = []
       
   157     for header, func in VALIDATORS:
       
   158         value = headers_in.getHeader(header)
       
   159         if value is not None:
       
   160             result.append((func, value))
       
   161     return result
       
   162 
       
   163 
       
   164 def if_modified_since(ref_date, headers_out):
       
   165     last_modified = headers_out.getHeader('last-modified')
       
   166     if last_modified is None:
       
   167         return True
       
   168     return ref_date < last_modified
       
   169 
       
   170 def if_none_match(tags, headers_out):
       
   171     etag = headers_out.getHeader('etag')
       
   172     if etag is None:
       
   173         return True
       
   174     return not ((etag in tags) or ('*' in tags))
       
   175 
       
   176 VALIDATORS = [
       
   177     ('if-modified-since', if_modified_since),
       
   178     #('if-unmodified-since', if_unmodified_since),
       
   179     ('if-none-match', if_none_match),
       
   180     #('if-modified-since', if_modified_since),
       
   181 ]