Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/utils/cache.py: 15%

194 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1""" 

2This module contains helper functions for controlling caching. It does so by 

3managing the "Vary" header of responses. It includes functions to patch the 

4header of response objects directly and decorators that change functions to do 

5that header-patching themselves. 

6 

7For information on the Vary header, see: 

8 

9 https://tools.ietf.org/html/rfc7231#section-7.1.4 

10 

11Essentially, the "Vary" HTTP header defines which headers a cache should take 

12into account when building its cache key. Requests with the same path but 

13different header content for headers named in "Vary" need to get different 

14cache keys to prevent delivery of wrong content. 

15 

16An example: i18n middleware would need to distinguish caches by the 

17"Accept-language" header. 

18""" 

19import hashlib 

20import time 

21from collections import defaultdict 

22 

23from django.conf import settings 

24from django.core.cache import caches 

25from django.http import HttpResponse, HttpResponseNotModified 

26from django.utils.http import http_date, parse_etags, parse_http_date_safe, quote_etag 

27from django.utils.log import log_response 

28from django.utils.regex_helper import _lazy_re_compile 

29from django.utils.timezone import get_current_timezone_name 

30from django.utils.translation import get_language 

31 

32cc_delim_re = _lazy_re_compile(r"\s*,\s*") 

33 

34 

35def patch_cache_control(response, **kwargs): 

36 """ 

37 Patch the Cache-Control header by adding all keyword arguments to it. 

38 The transformation is as follows: 

39 

40 * All keyword parameter names are turned to lowercase, and underscores 

41 are converted to hyphens. 

42 * If the value of a parameter is True (exactly True, not just a 

43 true value), only the parameter name is added to the header. 

44 * All other parameters are added with their value, after applying 

45 str() to it. 

46 """ 

47 

48 def dictitem(s): 

49 t = s.split("=", 1) 

50 if len(t) > 1: 

51 return (t[0].lower(), t[1]) 

52 else: 

53 return (t[0].lower(), True) 

54 

55 def dictvalue(*t): 

56 if t[1] is True: 

57 return t[0] 

58 else: 

59 return "%s=%s" % (t[0], t[1]) 

60 

61 cc = defaultdict(set) 

62 if response.get("Cache-Control"): 

63 for field in cc_delim_re.split(response.headers["Cache-Control"]): 

64 directive, value = dictitem(field) 

65 if directive == "no-cache": 

66 # no-cache supports multiple field names. 

67 cc[directive].add(value) 

68 else: 

69 cc[directive] = value 

70 

71 # If there's already a max-age header but we're being asked to set a new 

72 # max-age, use the minimum of the two ages. In practice this happens when 

73 # a decorator and a piece of middleware both operate on a given view. 

74 if "max-age" in cc and "max_age" in kwargs: 

75 kwargs["max_age"] = min(int(cc["max-age"]), kwargs["max_age"]) 

76 

77 # Allow overriding private caching and vice versa 

78 if "private" in cc and "public" in kwargs: 

79 del cc["private"] 

80 elif "public" in cc and "private" in kwargs: 

81 del cc["public"] 

82 

83 for (k, v) in kwargs.items(): 

84 directive = k.replace("_", "-") 

85 if directive == "no-cache": 

86 # no-cache supports multiple field names. 

87 cc[directive].add(v) 

88 else: 

89 cc[directive] = v 

90 

91 directives = [] 

92 for directive, values in cc.items(): 

93 if isinstance(values, set): 

94 if True in values: 

95 # True takes precedence. 

96 values = {True} 

97 directives.extend([dictvalue(directive, value) for value in values]) 

98 else: 

99 directives.append(dictvalue(directive, values)) 

100 cc = ", ".join(directives) 

101 response.headers["Cache-Control"] = cc 

102 

103 

104def get_max_age(response): 

105 """ 

106 Return the max-age from the response Cache-Control header as an integer, 

107 or None if it wasn't found or wasn't an integer. 

108 """ 

109 if not response.has_header("Cache-Control"): 

110 return 

111 cc = dict( 

112 _to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"]) 

113 ) 

114 try: 

115 return int(cc["max-age"]) 

116 except (ValueError, TypeError, KeyError): 

117 pass 

118 

119 

120def set_response_etag(response): 

121 if not response.streaming and response.content: 

122 response.headers["ETag"] = quote_etag(hashlib.md5(response.content).hexdigest()) 

123 return response 

124 

125 

126def _precondition_failed(request): 

127 response = HttpResponse(status=412) 

128 log_response( 

129 "Precondition Failed: %s", 

130 request.path, 

131 response=response, 

132 request=request, 

133 ) 

134 return response 

135 

136 

137def _not_modified(request, response=None): 

138 new_response = HttpResponseNotModified() 

139 if response: 

140 # Preserve the headers required by Section 4.1 of RFC 7232, as well as 

141 # Last-Modified. 

142 for header in ( 

143 "Cache-Control", 

144 "Content-Location", 

145 "Date", 

146 "ETag", 

147 "Expires", 

148 "Last-Modified", 

149 "Vary", 

150 ): 

151 if header in response: 

152 new_response.headers[header] = response.headers[header] 

153 

154 # Preserve cookies as per the cookie specification: "If a proxy server 

155 # receives a response which contains a Set-cookie header, it should 

156 # propagate the Set-cookie header to the client, regardless of whether 

157 # the response was 304 (Not Modified) or 200 (OK). 

158 # https://curl.haxx.se/rfc/cookie_spec.html 

159 new_response.cookies = response.cookies 

160 return new_response 

161 

162 

163def get_conditional_response(request, etag=None, last_modified=None, response=None): 

164 # Only return conditional responses on successful requests. 

165 if response and not (200 <= response.status_code < 300): 

166 return response 

167 

168 # Get HTTP request headers. 

169 if_match_etags = parse_etags(request.META.get("HTTP_IF_MATCH", "")) 

170 if_unmodified_since = request.META.get("HTTP_IF_UNMODIFIED_SINCE") 

171 if_unmodified_since = if_unmodified_since and parse_http_date_safe( 

172 if_unmodified_since 

173 ) 

174 if_none_match_etags = parse_etags(request.META.get("HTTP_IF_NONE_MATCH", "")) 

175 if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE") 

176 if_modified_since = if_modified_since and parse_http_date_safe(if_modified_since) 

177 

178 # Step 1 of section 6 of RFC 7232: Test the If-Match precondition. 

179 if if_match_etags and not _if_match_passes(etag, if_match_etags): 

180 return _precondition_failed(request) 

181 

182 # Step 2: Test the If-Unmodified-Since precondition. 

183 if ( 

184 not if_match_etags 

185 and if_unmodified_since 

186 and not _if_unmodified_since_passes(last_modified, if_unmodified_since) 

187 ): 

188 return _precondition_failed(request) 

189 

190 # Step 3: Test the If-None-Match precondition. 

191 if if_none_match_etags and not _if_none_match_passes(etag, if_none_match_etags): 

192 if request.method in ("GET", "HEAD"): 

193 return _not_modified(request, response) 

194 else: 

195 return _precondition_failed(request) 

196 

197 # Step 4: Test the If-Modified-Since precondition. 

198 if ( 

199 not if_none_match_etags 

200 and if_modified_since 

201 and not _if_modified_since_passes(last_modified, if_modified_since) 

202 and request.method in ("GET", "HEAD") 

203 ): 

204 return _not_modified(request, response) 

205 

206 # Step 5: Test the If-Range precondition (not supported). 

207 # Step 6: Return original response since there isn't a conditional response. 

208 return response 

209 

210 

211def _if_match_passes(target_etag, etags): 

212 """ 

213 Test the If-Match comparison as defined in section 3.1 of RFC 7232. 

214 """ 

215 if not target_etag: 

216 # If there isn't an ETag, then there can't be a match. 

217 return False 

218 elif etags == ["*"]: 

219 # The existence of an ETag means that there is "a current 

220 # representation for the target resource", even if the ETag is weak, 

221 # so there is a match to '*'. 

222 return True 

223 elif target_etag.startswith("W/"): 

224 # A weak ETag can never strongly match another ETag. 

225 return False 

226 else: 

227 # Since the ETag is strong, this will only return True if there's a 

228 # strong match. 

229 return target_etag in etags 

230 

231 

232def _if_unmodified_since_passes(last_modified, if_unmodified_since): 

233 """ 

234 Test the If-Unmodified-Since comparison as defined in section 3.4 of 

235 RFC 7232. 

236 """ 

237 return last_modified and last_modified <= if_unmodified_since 

238 

239 

240def _if_none_match_passes(target_etag, etags): 

241 """ 

242 Test the If-None-Match comparison as defined in section 3.2 of RFC 7232. 

243 """ 

244 if not target_etag: 

245 # If there isn't an ETag, then there isn't a match. 

246 return True 

247 elif etags == ["*"]: 

248 # The existence of an ETag means that there is "a current 

249 # representation for the target resource", so there is a match to '*'. 

250 return False 

251 else: 

252 # The comparison should be weak, so look for a match after stripping 

253 # off any weak indicators. 

254 target_etag = target_etag.strip("W/") 

255 etags = (etag.strip("W/") for etag in etags) 

256 return target_etag not in etags 

257 

258 

259def _if_modified_since_passes(last_modified, if_modified_since): 

260 """ 

261 Test the If-Modified-Since comparison as defined in section 3.3 of RFC 7232. 

262 """ 

263 return not last_modified or last_modified > if_modified_since 

264 

265 

266def patch_response_headers(response, cache_timeout=None): 

267 """ 

268 Add HTTP caching headers to the given HttpResponse: Expires and 

269 Cache-Control. 

270 

271 Each header is only added if it isn't already set. 

272 

273 cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used 

274 by default. 

275 """ 

276 if cache_timeout is None: 

277 cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS 

278 if cache_timeout < 0: 

279 cache_timeout = 0 # Can't have max-age negative 

280 if not response.has_header("Expires"): 

281 response.headers["Expires"] = http_date(time.time() + cache_timeout) 

282 patch_cache_control(response, max_age=cache_timeout) 

283 

284 

285def add_never_cache_headers(response): 

286 """ 

287 Add headers to a response to indicate that a page should never be cached. 

288 """ 

289 patch_response_headers(response, cache_timeout=-1) 

290 patch_cache_control( 

291 response, no_cache=True, no_store=True, must_revalidate=True, private=True 

292 ) 

293 

294 

295def patch_vary_headers(response, newheaders): 

296 """ 

297 Add (or update) the "Vary" header in the given HttpResponse object. 

298 newheaders is a list of header names that should be in "Vary". If headers 

299 contains an asterisk, then "Vary" header will consist of a single asterisk 

300 '*'. Otherwise, existing headers in "Vary" aren't removed. 

301 """ 

302 # Note that we need to keep the original order intact, because cache 

303 # implementations may rely on the order of the Vary contents in, say, 

304 # computing an MD5 hash. 

305 if response.has_header("Vary"): 

306 vary_headers = cc_delim_re.split(response.headers["Vary"]) 

307 else: 

308 vary_headers = [] 

309 # Use .lower() here so we treat headers as case-insensitive. 

310 existing_headers = {header.lower() for header in vary_headers} 

311 additional_headers = [ 

312 newheader 

313 for newheader in newheaders 

314 if newheader.lower() not in existing_headers 

315 ] 

316 vary_headers += additional_headers 

317 if "*" in vary_headers: 317 ↛ 318line 317 didn't jump to line 318, because the condition on line 317 was never true

318 response.headers["Vary"] = "*" 

319 else: 

320 response.headers["Vary"] = ", ".join(vary_headers) 

321 

322 

323def has_vary_header(response, header_query): 

324 """ 

325 Check to see if the response has a given header name in its Vary header. 

326 """ 

327 if not response.has_header("Vary"): 

328 return False 

329 vary_headers = cc_delim_re.split(response.headers["Vary"]) 

330 existing_headers = {header.lower() for header in vary_headers} 

331 return header_query.lower() in existing_headers 

332 

333 

334def _i18n_cache_key_suffix(request, cache_key): 

335 """If necessary, add the current locale or time zone to the cache key.""" 

336 if settings.USE_I18N: 

337 # first check if LocaleMiddleware or another middleware added 

338 # LANGUAGE_CODE to request, then fall back to the active language 

339 # which in turn can also fall back to settings.LANGUAGE_CODE 

340 cache_key += ".%s" % getattr(request, "LANGUAGE_CODE", get_language()) 

341 if settings.USE_TZ: 

342 cache_key += ".%s" % get_current_timezone_name() 

343 return cache_key 

344 

345 

346def _generate_cache_key(request, method, headerlist, key_prefix): 

347 """Return a cache key from the headers given in the header list.""" 

348 ctx = hashlib.md5() 

349 for header in headerlist: 

350 value = request.META.get(header) 

351 if value is not None: 

352 ctx.update(value.encode()) 

353 url = hashlib.md5(request.build_absolute_uri().encode("ascii")) 

354 cache_key = "views.decorators.cache.cache_page.%s.%s.%s.%s" % ( 

355 key_prefix, 

356 method, 

357 url.hexdigest(), 

358 ctx.hexdigest(), 

359 ) 

360 return _i18n_cache_key_suffix(request, cache_key) 

361 

362 

363def _generate_cache_header_key(key_prefix, request): 

364 """Return a cache key for the header cache.""" 

365 url = hashlib.md5(request.build_absolute_uri().encode("ascii")) 

366 cache_key = "views.decorators.cache.cache_header.%s.%s" % ( 

367 key_prefix, 

368 url.hexdigest(), 

369 ) 

370 return _i18n_cache_key_suffix(request, cache_key) 

371 

372 

373def get_cache_key(request, key_prefix=None, method="GET", cache=None): 

374 """ 

375 Return a cache key based on the request URL and query. It can be used 

376 in the request phase because it pulls the list of headers to take into 

377 account from the global URL registry and uses those to build a cache key 

378 to check against. 

379 

380 If there isn't a headerlist stored, return None, indicating that the page 

381 needs to be rebuilt. 

382 """ 

383 if key_prefix is None: 

384 key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX 

385 cache_key = _generate_cache_header_key(key_prefix, request) 

386 if cache is None: 

387 cache = caches[settings.CACHE_MIDDLEWARE_ALIAS] 

388 headerlist = cache.get(cache_key) 

389 if headerlist is not None: 

390 return _generate_cache_key(request, method, headerlist, key_prefix) 

391 else: 

392 return None 

393 

394 

395def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cache=None): 

396 """ 

397 Learn what headers to take into account for some request URL from the 

398 response object. Store those headers in a global URL registry so that 

399 later access to that URL will know what headers to take into account 

400 without building the response object itself. The headers are named in the 

401 Vary header of the response, but we want to prevent response generation. 

402 

403 The list of headers to use for cache key generation is stored in the same 

404 cache as the pages themselves. If the cache ages some data out of the 

405 cache, this just means that we have to build the response once to get at 

406 the Vary header and so at the list of headers to use for the cache key. 

407 """ 

408 if key_prefix is None: 

409 key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX 

410 if cache_timeout is None: 

411 cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS 

412 cache_key = _generate_cache_header_key(key_prefix, request) 

413 if cache is None: 

414 cache = caches[settings.CACHE_MIDDLEWARE_ALIAS] 

415 if response.has_header("Vary"): 

416 is_accept_language_redundant = settings.USE_I18N 

417 # If i18n is used, the generated cache key will be suffixed with the 

418 # current locale. Adding the raw value of Accept-Language is redundant 

419 # in that case and would result in storing the same content under 

420 # multiple keys in the cache. See #18191 for details. 

421 headerlist = [] 

422 for header in cc_delim_re.split(response.headers["Vary"]): 

423 header = header.upper().replace("-", "_") 

424 if header != "ACCEPT_LANGUAGE" or not is_accept_language_redundant: 

425 headerlist.append("HTTP_" + header) 

426 headerlist.sort() 

427 cache.set(cache_key, headerlist, cache_timeout) 

428 return _generate_cache_key(request, request.method, headerlist, key_prefix) 

429 else: 

430 # if there is no Vary header, we still need a cache key 

431 # for the request.build_absolute_uri() 

432 cache.set(cache_key, [], cache_timeout) 

433 return _generate_cache_key(request, request.method, [], key_prefix) 

434 

435 

436def _to_tuple(s): 

437 t = s.split("=", 1) 

438 if len(t) == 2: 

439 return t[0].lower(), t[1] 

440 return t[0].lower(), True