Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 731 lines (548 sloc) 23.695 kB
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
1 import re
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
2 import time
3 import datetime
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
4 import logging
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
5 import dateutil.parser
6 import dateutil.tz
7 import wsgiref.handlers
8
9 from thread import allocate_lock
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
10
11 from zope.interface import alsoProvides
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
12 from zope.component import queryMultiAdapter
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
13 from zope.component import queryUtility
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
14 from zope.component import getUtility
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
15
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
16 from zope.annotation.interfaces import IAnnotations
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
17 from z3c.caching.interfaces import ILastModified
18 from plone.registry.interfaces import IRegistry
5894e3a @optilude Crude RAM cache.
optilude authored
19 from plone.memoize.interfaces import ICacheChooser
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
20
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
21 from plone.app.caching.interfaces import IRAMCached
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
22 from plone.app.caching.interfaces import IETagValue
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
23 from plone.app.caching.interfaces import IPloneCacheSettings
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
24
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
25 from AccessControl.PermissionRole import rolesForPermissionOn
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
26 from Products.CMFCore.interfaces import IContentish
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
27 from Products.CMFCore.interfaces import ISiteRoot
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
28
be619a7 @optilude Step two of the big refactoring.
optilude authored
29 PAGE_CACHE_KEY = 'plone.app.caching.operations.ramcache'
30 PAGE_CACHE_ANNOTATION_KEY = 'plone.app.caching.operations.ramcache.key'
43da568 saving the etag calculation in a request annotation
newbery authored
31 ETAG_ANNOTATION_KEY = 'plone.app.caching.operations.etag'
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
32 LASTMODIFIED_ANNOTATION_KEY = 'plone.app.caching.operations.lastmodified'
33 _marker = object()
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
34
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
35 logger = logging.getLogger('plone.app.caching')
36
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
37 parseETagLock = allocate_lock()
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
38 # etagQuote = re.compile('(\s*\"([^\"]*)\"\s*,{0,1})')
39 # etagNoQuote = re.compile('(\s*([^,]*)\s*,{0,1})')
40
41 etagQuote = re.compile('(\s*(W\/)?\"([^\"]*)\"\s*,?)')
42 etagNoQuote = re.compile('(\s*(W\/)?([^,]*)\s*,?)')
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
43
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
44 #
be619a7 @optilude Step two of the big refactoring.
optilude authored
45 # Operation helpers, used in the implementations of interceptResponse() and
46 # modifyResponse().
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
47 #
be619a7 @optilude Step two of the big refactoring.
optilude authored
48 # These all take three parameters, published, request and response, as well
49 # as any additional keyword parameters required.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
50 #
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
51
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
52 def setCacheHeaders(published, request, response, maxage=None, smaxage=None, etag=None, lastModified=None, vary=None):
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
53 """General purpose dispatcher to set various cache headers
54
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
55 ``maxage`` is the cache timeout value in seconds
56 ``smaxage`` is the proxy cache timeout value in seconds.
57 ``lastModified`` is a datetime object for the last modified time
58 ``etag`` is an etag string
59 ``vary`` is a vary header string
60 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
61
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
62 if maxage:
63 cacheInBrowserAndProxy(published, request, response, maxage, smaxage=smaxage,
64 etag=etag, lastModified=lastModified, vary=vary)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
65
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
66 elif smaxage:
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
67 cacheInProxy(published, request, response, smaxage,
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
68 etag=etag, lastModified=lastModified, vary=vary)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
69
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
70 elif etag or lastModified:
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
71 cacheInBrowser(published, request, response,
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
72 etag=etag, lastModified=lastModified)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
73
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
74 else:
75 doNotCache(published, request, response)
76
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
77 def doNotCache(published, request, response):
78 """Set response headers to ensure that the response is not cached by
79 web browsers or caching proxies.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
80
1c27d62 A bit more explanation about what is happening in our doNotCache and …
newbery authored
81 This is an IE-safe operation. Under certain conditions, IE chokes on
82 ``no-cache`` and ``no-store`` cache-control tokens so instead we just
83 expire immediately and disable validation.
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
84 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
85
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
86 if response.getHeader('Last-Modified'):
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
87 del response.headers['last-modified']
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
88
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
89 response.setHeader('Expires', formatDateTime(getExpiration(0)))
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
90 response.setHeader('Cache-Control', 'max-age=0, must-revalidate, private')
91
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
92 def cacheInBrowser(published, request, response, etag=None, lastModified=None):
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
93 """Set response headers to indicate that browsers should cache the
1c27d62 A bit more explanation about what is happening in our doNotCache and …
newbery authored
94 response but expire immediately and revalidate the cache on every
95 subsequent request.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
96
9dfaa46 @optilude Clean up and test code TTW. Add abstraction for etag generation to av…
optilude authored
97 ``etag`` is a string value indicating an ETag to use.
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
98 ``lastModified`` is a datetime object
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
99
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
100 If neither etag nor lastModified is given then no validation is
1c27d62 A bit more explanation about what is happening in our doNotCache and …
newbery authored
101 possible and this becomes equivalent to doNotCache()
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
102 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
103
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
104 if etag is not None:
50b42a9 Some claim the spec requires etag values to be quoted. I think the s…
newbery authored
105 response.setHeader('ETag', '"%s"' %etag, literal=1)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
106
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
107 if lastModified is not None:
108 response.setHeader('Last-Modified', formatDateTime(lastModified))
4805448 Tightened up the Last=Modified header control. Still no joy on the im…
newbery authored
109 elif response.getHeader('Last-Modified'):
110 del response.headers['last-modified']
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
111
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
112 response.setHeader('Expires', formatDateTime(getExpiration(0)))
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
113 response.setHeader('Cache-Control', 'max-age=0, must-revalidate, private')
114
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
115 def cacheInProxy(published, request, response, smaxage, etag=None, lastModified=None, vary=None):
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
116 """Set headers to cache the response in a caching proxy.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
117
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
118 ``smaxage`` is the timeout value in seconds.
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
119 ``lastModified`` is a datetime object for the last modified time
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
120 ``etag`` is an etag string
121 ``vary`` is a vary header string
122 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
123
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
124 if lastModified is not None:
125 response.setHeader('Last-Modified', formatDateTime(lastModified))
4805448 Tightened up the Last=Modified header control. Still no joy on the im…
newbery authored
126 elif response.getHeader('Last-Modified'):
127 del response.headers['last-modified']
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
128
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
129 if etag is not None:
50b42a9 Some claim the spec requires etag values to be quoted. I think the s…
newbery authored
130 response.setHeader('ETag', '"%s"' %etag, literal=1)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
131
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
132 if vary is not None:
133 response.setHeader('Vary', vary)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
134
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
135 response.setHeader('Expires', formatDateTime(getExpiration(0)))
db93042 @optilude Beginning of tests for operations
optilude authored
136 response.setHeader('Cache-Control', 'max-age=0, s-maxage=%d, must-revalidate' % smaxage)
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
137
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
138 def cacheInBrowserAndProxy(published, request, response, maxage, smaxage=None, etag=None, lastModified=None, vary=None):
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
139 """Set headers to cache the response in the browser and caching proxy if
140 applicable.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
141
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
142 ``maxage`` is the timeout value in seconds
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
143 ``smaxage`` is the proxy timeout value in seconds
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
144 ``lastModified`` is a datetime object for the last modified time
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
145 ``etag`` is an etag string
146 ``vary`` is a vary header string
147 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
148
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
149 if lastModified is not None:
150 response.setHeader('Last-Modified', formatDateTime(lastModified))
4805448 Tightened up the Last=Modified header control. Still no joy on the im…
newbery authored
151 elif response.getHeader('Last-Modified'):
152 del response.headers['last-modified']
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
153
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
154 if etag is not None:
50b42a9 Some claim the spec requires etag values to be quoted. I think the s…
newbery authored
155 response.setHeader('ETag', '"%s"' %etag, literal=1)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
156
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
157 if vary is not None:
158 response.setHeader('Vary', vary)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
159
80278fd Fixing the wrong expires bug
newbery authored
160 response.setHeader('Expires', formatDateTime(getExpiration(maxage)))
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
161
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
162 if smaxage is not None:
163 maxage = '%s, s-maxage=%s' %(maxage, smaxage)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
164
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
165 # Substituting proxy-validate in place of must=revalidate here because of Safari bug
166 # https://bugs.webkit.org/show_bug.cgi?id=13128
167 response.setHeader('Cache-Control', 'max-age=%s, proxy-revalidate, public' % maxage)
7b4a002 A first draft emulating the CacheFu default policies. Registration d…
newbery authored
168
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
169 def cacheInRAM(published, request, response, etag=None, lastModified=None, annotationsKey=PAGE_CACHE_ANNOTATION_KEY):
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
170 """Set a flag indicating that the response for the given request
171 should be cached in RAM.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
172
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
173 This will signal to a transform chain step after the response has been
174 generated to store the result in the RAM cache.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
175
be619a7 @optilude Step two of the big refactoring.
optilude authored
176 To actually use the cached response, you can implement
177 ``interceptResponse()`` in your caching operation to call
178 ``fetchFromRAMCache()`` and then return the value of the
179 ``cachedResponse()`` helper.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
180
be619a7 @optilude Step two of the big refactoring.
optilude authored
181 ``etag`` is a string identifying the resource.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
182
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
183 ``annotationsKey`` is the key used by the transform to look up the
be619a7 @optilude Step two of the big refactoring.
optilude authored
184 caching key when storing the response in the cache. It should match that
185 passed to ``storeResponseInRAMCache()``.
5894e3a @optilude Crude RAM cache.
optilude authored
186 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
187
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
188 annotations = IAnnotations(request, None)
189 if annotations is None:
190 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
191
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
192 key = getRAMCacheKey(request, etag=etag, lastModified=lastModified)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
193
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
194 annotations[annotationsKey] = key
195 alsoProvides(request, IRAMCached)
196
de8c3ca @optilude Fix interaction between RAM cache and GZip
optilude authored
197 def cachedResponse(published, request, response, status, headers, body, gzip=False):
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
198 """Returned a cached page. Modifies the response (status and headers)
be619a7 @optilude Step two of the big refactoring.
optilude authored
199 and returns the cached body.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
200
db93042 @optilude Beginning of tests for operations
optilude authored
201 ``status`` is the cached HTTP status
202 ``headers`` is a dictionary of cached HTTP headers
203 ``body`` is a cached response body
de8c3ca @optilude Fix interaction between RAM cache and GZip
optilude authored
204 ``gzip`` should be set to True if the response is to be gzipped
be619a7 @optilude Step two of the big refactoring.
optilude authored
205 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
206
be619a7 @optilude Step two of the big refactoring.
optilude authored
207 response.setStatus(status)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
208
be619a7 @optilude Step two of the big refactoring.
optilude authored
209 for k, v in headers.items():
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
210 if k.lower() == 'etag':
be619a7 @optilude Step two of the big refactoring.
optilude authored
211 response.setHeader(k, v, literal=1)
212 else:
213 response.setHeader(k, v)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
214
8ffe8fa Retain empty tokens in the etag as placeholders to ensure uniqueness.
newbery authored
215 response.setHeader('X-RAMCache', PAGE_CACHE_KEY, literal=1)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
216
de8c3ca @optilude Fix interaction between RAM cache and GZip
optilude authored
217 if not gzip:
218 response.enableHTTPCompression(request, disable=True)
219 else:
220 response.enableHTTPCompression(request)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
221
be619a7 @optilude Step two of the big refactoring.
optilude authored
222 return body
223
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
224 def notModified(published, request, response, etag=None, lastModified=None):
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
225 """Return a ``304 NOT MODIFIED`` response. Modifies the response (status)
226 and returns an empty body to indicate the request should be interrupted.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
227
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
228 ``etag`` is an ETag to set on the response
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
229 ``lastModified`` is the last modified date to set on the response
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
230
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
231 Both ``etag`` and ``lastModified`` are optional.
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
232 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
233
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
234 if etag is not None:
235 response.setHeader('ETag', etag, literal=1)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
236
5439b08 The specs say that Last-Modified MUST NOT be included in a 304:
newbery authored
237 # Specs say that Last-Modified MUST NOT be included in a 304
e71ed05 Fixing the RR 304s
newbery authored
238 # and Cache-Control/Expires MUST NOT be included unless they
239 # differ from the original response. We'll delete all, including
240 # Expires although technically it should be included. This is
241 # probably okay since in the original we only include Expires
242 # along with a Cache-Control and HTTP/1.1 clients will always
243 # use the later over any Expires header anyway. HTTP/1.0 clients
244 # never send conditional requests so they will never see this.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
245 #
5439b08 The specs say that Last-Modified MUST NOT be included in a 304:
newbery authored
246 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
247 #
e71ed05 Fixing the RR 304s
newbery authored
248 if response.getHeader('Last-Modified'):
249 del response.headers['last-modified']
250 if response.getHeader('Expires'):
251 del response.headers['expires']
252 if response.getHeader('Cache-Control'):
253 del response.headers['cache-control']
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
254
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
255 response.setStatus(304)
256 return u""
257
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
258
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
259 #
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
260 # Cache checks
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
261 #
06ac774 @optilude Code cleanup and RAM cache refactoring
optilude authored
262
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
263 def cacheStop(request, rulename):
264 """Check for any cache stop variables in the request.
265 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
266
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
267 # Only cache GET requests
268 if request.get('REQUEST_METHOD') != 'GET':
269 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
270
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
271 # rss_search also uses the SearchableText variable
272 # so we'll hard code an exception for now
273 if rulename == 'plone.content.feed':
274 return False
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
275
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
276 registry = getUtility(IRegistry)
277 if registry is None:
278 return False
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
279
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
280 ploneSettings = registry.forInterface(IPloneCacheSettings)
281 variables = ploneSettings.cacheStopRequestVariables
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
282
bdb4e71 Add a knob to stop caching based on the existence of listed request v…
newbery authored
283 for variable in variables:
284 if request.has_key(variable):
285 return True
286 return False
287
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
288 def isModified(request, etag=None, lastModified=None):
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
289 """Return True or False depending on whether the published resource has
290 been modified.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
291
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
292 ``etag`` is the current etag, to be checked against the If-None-Match
293 header.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
294
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
295 ``lastModified`` is the current last-modified datetime, to be checked
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
296 against the If-Modified-Since header.
297 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
298
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
299 if not etag and not lastModified:
300 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
301
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
302 ifModifiedSince = request.getHeader('If-Modified-Since', None)
303 ifNoneMatch = request.getHeader('If-None-Match', None)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
304
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
305 if ifModifiedSince is None and ifNoneMatch is None:
306 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
307
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
308 etagMatched = False
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
309
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
310 # Check etags
311 if ifNoneMatch and etag is not None:
312 if not etag:
313 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
314
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
315 clientETags = parseETags(ifNoneMatch)
316 if not clientETags:
317 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
318
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
319 # is the current etag in the list of client-side etags?
320 if etag not in clientETags and '*' not in clientETags:
321 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
322
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
323 etagMatched = True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
324
093d72a @eleddy return modified if etags has been turned off since last page load.
eleddy authored
325 """
326 If a site turns off etags after having them on, the pages previously
a38a08c @gforcada Whitespaces cleanup
gforcada authored
327 served will return an If-None-Match header, but the site will not be
328 configured for etags. In this case, force a refresh to load the
329 latest headers. I interpret this as the spec rule that the
9356ad7 @eleddy test case for changing headers
eleddy authored
330 etags do NOT match, and therefor we must not return a 304.
093d72a @eleddy return modified if etags has been turned off since last page load.
eleddy authored
331 """
332 if ifNoneMatch and etag==None:
333 return True
334
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
335 # Check the modification date
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
336 if ifModifiedSince and lastModified is not None:
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
337
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
338 # Attempt to get a date
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
339
1be5d91 @optilude Tests for If-Modified-Since
optilude authored
340 ifModifiedSince = ifModifiedSince.split(';')[0]
341 ifModifiedSince = parseDateTime(ifModifiedSince)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
342
1be5d91 @optilude Tests for If-Modified-Since
optilude authored
343 if ifModifiedSince is None:
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
344 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
345
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
346 # has content been modified since the if-modified-since time?
347 try:
93709c9 Browser only knows the Last-Modified date to one second resolution
newbery authored
348 # browser only knows the date to one second resolution
5d1e657 Oops, previous fix bypassed the etag doublecheck. There was a reason…
newbery authored
349 if (lastModified - ifModifiedSince) > datetime.timedelta(seconds=1):
51c2a31 oops... lower case True
newbery authored
350 return True
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
351 except TypeError:
352 logger.exception("Could not compare dates")
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
353
1be5d91 @optilude Tests for If-Modified-Since
optilude authored
354 # If we expected an ETag and the client didn't give us one, consider
355 # that an error. This may be more conservative than the spec requires.
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
356 if etag is not None:
357 if not etagMatched:
358 return True
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
359
093d72a @eleddy return modified if etags has been turned off since last page load.
eleddy authored
360 # XXX Do we really want the default here to be false?
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
361 return False
362
a38a08c @gforcada Whitespaces cleanup
gforcada authored
363
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
364 def visibleToRole(published, role, permission='View'):
365 """Determine if the published object would be visible to the given
366 role.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
367
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
368 ``role`` is a role name, e.g. ``Anonymous``.
369 ``permission`` is the permission to check for.
370 """
371 return role in rolesForPermissionOn(permission, published)
372
be619a7 @optilude Step two of the big refactoring.
optilude authored
373 #
374 # Basic helper functions
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
375 #
be619a7 @optilude Step two of the big refactoring.
optilude authored
376
377 def getContext(published, marker=(IContentish, ISiteRoot,)):
378 """Given a published object, attempt to look up a context
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
379
be619a7 @optilude Step two of the big refactoring.
optilude authored
380 ``published`` is the object that was published.
381 ``marker`` is a marker interface to look for
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
382
be619a7 @optilude Step two of the big refactoring.
optilude authored
383 Returns an item providing ``marker`` or None, if it cannot be found.
384 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
385
be619a7 @optilude Step two of the big refactoring.
optilude authored
386 if not isinstance(marker, (list, tuple,)):
387 marker = (marker,)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
388
be619a7 @optilude Step two of the big refactoring.
optilude authored
389 def checkType(context):
390 for m in marker:
391 if m.providedBy(context):
392 return True
393 return False
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
394
be619a7 @optilude Step two of the big refactoring.
optilude authored
395 while (
396 published is not None
397 and not checkType(published)
398 and hasattr(published, '__parent__',)
399 ):
400 published = published.__parent__
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
401
be619a7 @optilude Step two of the big refactoring.
optilude authored
402 if not checkType(published):
403 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
404
be619a7 @optilude Step two of the big refactoring.
optilude authored
405 return published
406
407 def formatDateTime(dt):
408 """Format a Python datetime object as an RFC1123 date.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
409
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
410 If the datetime object is timezone-naive, it is assumed to be local time.
be619a7 @optilude Step two of the big refactoring.
optilude authored
411 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
412
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
413 # We have to pass local time to format_date_time()
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
414
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
415 if dt.tzinfo is not None:
416 dt = dt.astimezone(dateutil.tz.tzlocal())
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
417
be619a7 @optilude Step two of the big refactoring.
optilude authored
418 return wsgiref.handlers.format_date_time(time.mktime(dt.timetuple()))
419
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
420 def parseDateTime(str):
421 """Return a Python datetime object from an an RFC1123 date.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
422
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
423 Returns a datetime object with a timezone. If no timezone is found in the
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
424 input string, assume local time.
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
425 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
426
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
427 try:
428 dt = dateutil.parser.parse(str)
429 except ValueError:
430 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
431
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
432 if not dt:
433 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
434
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
435 if dt.tzinfo is None:
436 dt = datetime.datetime(dt.year, dt.month, dt.day,
437 dt.hour, dt.minute, dt.second, dt.microsecond,
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
438 dateutil.tz.tzlocal())
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
439
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
440 return dt
441
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
442 def getLastModifiedAnnotation(published, request, lastModified=True):
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
443 """Try to get the last modified date from a request annotation if available,
444 otherwise try to get it from published object
445 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
446
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
447 if not lastModified:
448 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
449
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
450 annotations = IAnnotations(request, None)
451 if annotations is not None:
452 dt = annotations.get(LASTMODIFIED_ANNOTATION_KEY, _marker)
453 if dt is not _marker:
454 return dt
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
455
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
456 dt = getLastModified(published, lastModified=lastModified)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
457
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
458 if annotations is not None:
459 annotations[LASTMODIFIED_ANNOTATION_KEY] = dt
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
460
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
461 return dt
462
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
463 def getLastModified(published, lastModified=True):
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
464 """Get a last modified date or None.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
465
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
466 If an ``ILastModified`` adapter can be found, and returns a date that is
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
467 not timezone aware, assume it is local time and add timezone.
be619a7 @optilude Step two of the big refactoring.
optilude authored
468 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
469
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
470 if not lastModified:
471 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
472
be619a7 @optilude Step two of the big refactoring.
optilude authored
473 lastModified = ILastModified(published, None)
474 if lastModified is None:
475 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
476
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
477 dt = lastModified()
478 if dt is None:
479 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
480
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
481 if dt.tzinfo is None:
482 dt = datetime.datetime(dt.year, dt.month, dt.day,
483 dt.hour, dt.minute, dt.second, dt.microsecond,
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
484 dateutil.tz.tzlocal())
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
485
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
486 return dt
be619a7 @optilude Step two of the big refactoring.
optilude authored
487
488 def getExpiration(maxage):
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
489 """Get an expiration date as a datetime in the local timezone.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
490
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
491 ``maxage`` is the maximum age of the item, in seconds. If it is 0 or
492 negative, return a date ten years in the past.
be619a7 @optilude Step two of the big refactoring.
optilude authored
493 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
494
be619a7 @optilude Step two of the big refactoring.
optilude authored
495 now = datetime.datetime.now()
496 if maxage > 0:
497 return now + datetime.timedelta(seconds=maxage)
498 else:
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
499 return now - datetime.timedelta(days=3650)
be619a7 @optilude Step two of the big refactoring.
optilude authored
500
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
501 def getETagAnnotation(published, request, keys=(), extraTokens=()):
502 """Try to get the ETag from a request annotation if available,
503 otherwise try to get it from published object
504 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
505
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
506 if not keys and not extraTokens:
507 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
508
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
509 annotations = IAnnotations(request, None)
510 if annotations is not None:
511 etag = annotations.get(ETAG_ANNOTATION_KEY, _marker)
512 if etag is not _marker:
513 return etag
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
514
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
515 etag = getETag(published, request, keys=keys, extraTokens=extraTokens)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
516
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
517 if annotations is not None:
518 annotations[ETAG_ANNOTATION_KEY] = etag
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
519
e7c3115 Refactor the etag annotation lookup into a wrapper function in case s…
newbery authored
520 return etag
521
be619a7 @optilude Step two of the big refactoring.
optilude authored
522 def getETag(published, request, keys=(), extraTokens=()):
523 """Calculate an ETag.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
524
be619a7 @optilude Step two of the big refactoring.
optilude authored
525 ``keys`` is a list of types of items to include in the ETag. These must
526 match named multi-adapters on (published, request) providing
527 ``IETagValue``.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
528
be619a7 @optilude Step two of the big refactoring.
optilude authored
529 ``extraTokens`` is a list of additional ETag tokens to include, verbatim
530 as strings.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
531
be619a7 @optilude Step two of the big refactoring.
optilude authored
532 All tokens will be concatenated into an ETag string, separated by pipes.
533 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
534
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
535 if not keys and not extraTokens:
536 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
537
be619a7 @optilude Step two of the big refactoring.
optilude authored
538 tokens = []
8ffe8fa Retain empty tokens in the etag as placeholders to ensure uniqueness.
newbery authored
539 noTokens = True
be619a7 @optilude Step two of the big refactoring.
optilude authored
540 for key in keys:
541 component = queryMultiAdapter((published, request), IETagValue, name=key)
542 if component is None:
543 logger.warning("Could not find value adapter for ETag component %s", key)
8ffe8fa Retain empty tokens in the etag as placeholders to ensure uniqueness.
newbery authored
544 tokens.append('')
be619a7 @optilude Step two of the big refactoring.
optilude authored
545 else:
546 value = component()
8ffe8fa Retain empty tokens in the etag as placeholders to ensure uniqueness.
newbery authored
547 if value is None:
548 value = ''
549 else:
550 noTokens = False
551 tokens.append(value)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
552
be619a7 @optilude Step two of the big refactoring.
optilude authored
553 for token in extraTokens:
8ffe8fa Retain empty tokens in the etag as placeholders to ensure uniqueness.
newbery authored
554 noTokens = False
be619a7 @optilude Step two of the big refactoring.
optilude authored
555 tokens.append(token)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
556
8ffe8fa Retain empty tokens in the etag as placeholders to ensure uniqueness.
newbery authored
557 if noTokens:
b2d51e3 no tokens = no etag
newbery authored
558 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
559
be619a7 @optilude Step two of the big refactoring.
optilude authored
560 etag = '|' + '|'.join(tokens)
50b42a9 Some claim the spec requires etag values to be quoted. I think the s…
newbery authored
561 etag = etag.replace(',', ';') # commas are bad in etags
562 etag = etag.replace('"', "'") # double quotes are bad in etags
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
563
50b42a9 Some claim the spec requires etag values to be quoted. I think the s…
newbery authored
564 return etag
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
565
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
566 def parseETags(text, allowWeak=True, _result=None):
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
567 """Parse a header value into a list of etags. Handles fishy quoting and
568 other browser quirks.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
569
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
570 ``text`` is the header value to parse.
571 ``allowWeak`` should be False if weak ETag values should not be returned
572 ``_result`` is internal - don't set it.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
573
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
574 Returns a list of strings. For weak etags, the W/ prefix is removed.
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
575 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
576
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
577 result = _result
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
578
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
579 if result is None:
580 result = []
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
581
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
582 if not text:
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
583 return result
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
584
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
585 # Lock, since regular expressions are not threadsafe
586 parseETagLock.acquire()
587 try:
588 m = etagQuote.match(text)
589 if m:
590 # Match quoted etag (spec-observing client)
591 l = len(m.group(1))
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
592 value = (m.group(2) or '') + (m.group(3) or '')
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
593 else:
594 # Match non-quoted etag (lazy client)
595 m = etagNoQuote.match(text)
596 if m:
597 l = len(m.group(1))
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
598 value = (m.group(2) or '') + (m.group(3) or '')
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
599 else:
600 return result
601 finally:
602 parseETagLock.release()
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
603
cf07a08 @optilude Initial implementation of 304 responses with the new interceptor appr…
optilude authored
604 if value:
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
605
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
606 if value.startswith('W/'):
607 if allowWeak:
608 result.append(value[2:])
609 else:
610 result.append(value)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
611
41b162b @optilude More tests galore and a few bug fixes to come with them.
optilude authored
612 return parseETags(text[l:], allowWeak=allowWeak, _result=result)
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
613
614 #
615 # RAM cache management
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
616 #
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
617
618 def getRAMCache(globalKey=PAGE_CACHE_KEY):
619 """Get a RAM cache instance for the given key. The return value is ``None``
620 if no RAM cache can be found, or a mapping object supporting at least
621 ``__getitem__()``, ``__setitem__()`` and ``get()`` that can be used to get
622 or set cache values.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
623
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
624 ``key`` is the global cache key, which must be unique site-wide. Most
625 commonly, this will be the operation dotted name.
626 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
627
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
628 chooser = queryUtility(ICacheChooser)
629 if chooser is None:
630 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
631
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
632 return chooser(globalKey)
633
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
634 def getRAMCacheKey(request, etag=None, lastModified=None):
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
635 """Calculate the cache key for pages cached in RAM.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
636
637 ``etag`` is a unique etag string.
638
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
639 ``lastModified`` is a datetime object giving the last=modified
640 date for the resource.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
641
de8edcf @lrowe Add the URL in the RAM cache key.
lrowe authored
642 The cache key is a combination of the resource's URL, the etag,
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
643 and the last-modified date. Both the etag and last=modified are
644 optional but in most cases that are worth caching in RAM, the etag
645 is needed to ensure the key changes when the resource view changes.
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
646 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
647
b6271ce @lrowe Easier to test RAM cache key.
lrowe authored
648 resourceKey = "%s%s?%s" % (
649 request.get('SERVER_URL', ''),
650 request.get('PATH_INFO', ''),
651 request.get('QUERY_STRING', ''),
652 )
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
653 if etag:
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
654 resourceKey = '|' + etag + '||' + resourceKey
655 if lastModified:
656 resourceKey = '|' + str(lastModified) + '||' + resourceKey
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
657 return resourceKey
658
659 def storeResponseInRAMCache(request, response, result, globalKey=PAGE_CACHE_KEY, annotationsKey=PAGE_CACHE_ANNOTATION_KEY):
660 """Store the given response in the RAM cache.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
661
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
662 ``result`` should be the response body as a string.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
663
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
664 ``globalKey`` is the global cache key. This needs to be the same key
665 as the one used to fetch the data.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
666
667 ``annotationsKey`` is the key in annotations on the request from which
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
668 the (resource-identifying) caching key should be retrieved. The default
669 is that used by the ``cacheInRAM()`` helper function.
670 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
671
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
672 annotations = IAnnotations(request, None)
673 if annotations is None:
674 return
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
675
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
676 key = annotations.get(annotationsKey)
677 if not key:
678 return
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
679
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
680 cache = getRAMCache(globalKey)
681 if cache is None:
682 return
a38a08c @gforcada Whitespaces cleanup
gforcada authored
683
dd60cec @spanktar dont cache things without a body or fear the wrath of the spinning br…
spanktar authored
684 """
a38a08c @gforcada Whitespaces cleanup
gforcada authored
685 Resource registries have no body. If we put them in the cache the content
686 type headers will indicate length and the body will be '', causing the browser
dd60cec @spanktar dont cache things without a body or fear the wrath of the spinning br…
spanktar authored
687 to just spin. Furthermore, I doubt we ever want to cache an empty result:
688 it's an indication that something went wrong somewhere.
689
a38a08c @gforcada Whitespaces cleanup
gforcada authored
690 This does mean that any resources will not be cached in ram. There is
691 potentially another fix but I doubt long term it's ever the right thing to
dd60cec @spanktar dont cache things without a body or fear the wrath of the spinning br…
spanktar authored
692 do.
693 """
694 if result == '':
a38a08c @gforcada Whitespaces cleanup
gforcada authored
695 return
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
696
31282e1 @optilude Skeletal tests for all the operations and etag code that's gone in re…
optilude authored
697 status = response.getStatus()
698 headers = dict(request.response.headers)
de8c3ca @optilude Fix interaction between RAM cache and GZip
optilude authored
699 gzipFlag = response.enableHTTPCompression(query=True)
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
700
de8c3ca @optilude Fix interaction between RAM cache and GZip
optilude authored
701 cache[key] = (status, headers, result, gzipFlag)
ce6e84b @optilude RAM cache tests
optilude authored
702
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
703 def fetchFromRAMCache(request, etag=None, lastModified=None, globalKey=PAGE_CACHE_KEY, default=None):
ce6e84b @optilude RAM cache tests
optilude authored
704 """Return a page cached in RAM, or None if it cannot be found.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
705
de8c3ca @optilude Fix interaction between RAM cache and GZip
optilude authored
706 The return value is a tuple as stored by ``storeResponseInRAMCache()``.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
707
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
708 ``etag`` is an ETag for the content, and is usually used as a basis for
709 the cache key.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
710
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
711 ``lastModified`` is the last modified date for the content, which can
712 potentially be used instead of etag if sufficient to ensure freshness.
713 Perhaps a rare occurance but it's here in case someone needs it.
714 Do not use this to cache binary responses (like images and file downloads)
715 as Zope already caches most of the payload of these.
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
716
ce6e84b @optilude RAM cache tests
optilude authored
717 ``globalKey`` is the global cache key. This needs to be the same key
718 as the one used to store the data, i.e. it must correspond to the one
719 used when calling ``storeResponseInRAMCache()``.
720 """
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
721
ce6e84b @optilude RAM cache tests
optilude authored
722 cache = getRAMCache(globalKey)
723 if cache is None:
724 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
725
e18841b Major refactoring of the cache operations. Fewer operations and more…
newbery authored
726 key = getRAMCacheKey(request, etag=etag, lastModified=lastModified)
ce6e84b @optilude RAM cache tests
optilude authored
727 if key is None:
728 return None
1bcf568 @mauritsvanrees Nuke trailing white space.
mauritsvanrees authored
729
ce6e84b @optilude RAM cache tests
optilude authored
730 return cache.get(key, default)
Something went wrong with that request. Please try again.