|
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 ] |