| Leo Repp | 58b9f11 | 2021-11-22 11:57:47 +0100 | [diff] [blame^] | 1 | 'use strict'; |
| 2 | // rfc7231 6.1 |
| 3 | |
| 4 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } |
| 5 | |
| 6 | var statusCodeCacheableByDefault = [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501]; |
| 7 | |
| 8 | // This implementation does not understand partial responses (206) |
| 9 | var understoodStatuses = [200, 203, 204, 300, 301, 302, 303, 307, 308, 404, 405, 410, 414, 501]; |
| 10 | |
| 11 | var hopByHopHeaders = { 'connection': true, 'keep-alive': true, 'proxy-authenticate': true, 'proxy-authorization': true, 'te': true, 'trailer': true, 'transfer-encoding': true, 'upgrade': true }; |
| 12 | var excludedFromRevalidationUpdate = { |
| 13 | // Since the old body is reused, it doesn't make sense to change properties of the body |
| 14 | 'content-length': true, 'content-encoding': true, 'transfer-encoding': true, |
| 15 | 'content-range': true |
| 16 | }; |
| 17 | |
| 18 | function parseCacheControl(header) { |
| 19 | var cc = {}; |
| 20 | if (!header) return cc; |
| 21 | |
| 22 | // TODO: When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives), |
| 23 | // the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale |
| 24 | var parts = header.trim().split(/\s*,\s*/); // TODO: lame parsing |
| 25 | for (var _iterator = parts, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) { |
| 26 | var _ref; |
| 27 | |
| 28 | if (_isArray) { |
| 29 | if (_i >= _iterator.length) break; |
| 30 | _ref = _iterator[_i++]; |
| 31 | } else { |
| 32 | _i = _iterator.next(); |
| 33 | if (_i.done) break; |
| 34 | _ref = _i.value; |
| 35 | } |
| 36 | |
| 37 | var part = _ref; |
| 38 | |
| 39 | var _part$split = part.split(/\s*=\s*/, 2), |
| 40 | k = _part$split[0], |
| 41 | v = _part$split[1]; |
| 42 | |
| 43 | cc[k] = v === undefined ? true : v.replace(/^"|"$/g, ''); // TODO: lame unquoting |
| 44 | } |
| 45 | |
| 46 | return cc; |
| 47 | } |
| 48 | |
| 49 | function formatCacheControl(cc) { |
| 50 | var parts = []; |
| 51 | for (var k in cc) { |
| 52 | var v = cc[k]; |
| 53 | parts.push(v === true ? k : k + '=' + v); |
| 54 | } |
| 55 | if (!parts.length) { |
| 56 | return undefined; |
| 57 | } |
| 58 | return parts.join(', '); |
| 59 | } |
| 60 | |
| 61 | module.exports = function () { |
| 62 | function CachePolicy(req, res) { |
| 63 | var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, |
| 64 | shared = _ref2.shared, |
| 65 | cacheHeuristic = _ref2.cacheHeuristic, |
| 66 | immutableMinTimeToLive = _ref2.immutableMinTimeToLive, |
| 67 | ignoreCargoCult = _ref2.ignoreCargoCult, |
| 68 | _fromObject = _ref2._fromObject; |
| 69 | |
| 70 | _classCallCheck(this, CachePolicy); |
| 71 | |
| 72 | if (_fromObject) { |
| 73 | this._fromObject(_fromObject); |
| 74 | return; |
| 75 | } |
| 76 | |
| 77 | if (!res || !res.headers) { |
| 78 | throw Error("Response headers missing"); |
| 79 | } |
| 80 | this._assertRequestHasHeaders(req); |
| 81 | |
| 82 | this._responseTime = this.now(); |
| 83 | this._isShared = shared !== false; |
| 84 | this._cacheHeuristic = undefined !== cacheHeuristic ? cacheHeuristic : 0.1; // 10% matches IE |
| 85 | this._immutableMinTtl = undefined !== immutableMinTimeToLive ? immutableMinTimeToLive : 24 * 3600 * 1000; |
| 86 | |
| 87 | this._status = 'status' in res ? res.status : 200; |
| 88 | this._resHeaders = res.headers; |
| 89 | this._rescc = parseCacheControl(res.headers['cache-control']); |
| 90 | this._method = 'method' in req ? req.method : 'GET'; |
| 91 | this._url = req.url; |
| 92 | this._host = req.headers.host; |
| 93 | this._noAuthorization = !req.headers.authorization; |
| 94 | this._reqHeaders = res.headers.vary ? req.headers : null; // Don't keep all request headers if they won't be used |
| 95 | this._reqcc = parseCacheControl(req.headers['cache-control']); |
| 96 | |
| 97 | // Assume that if someone uses legacy, non-standard uncecessary options they don't understand caching, |
| 98 | // so there's no point stricly adhering to the blindly copy&pasted directives. |
| 99 | if (ignoreCargoCult && "pre-check" in this._rescc && "post-check" in this._rescc) { |
| 100 | delete this._rescc['pre-check']; |
| 101 | delete this._rescc['post-check']; |
| 102 | delete this._rescc['no-cache']; |
| 103 | delete this._rescc['no-store']; |
| 104 | delete this._rescc['must-revalidate']; |
| 105 | this._resHeaders = Object.assign({}, this._resHeaders, { 'cache-control': formatCacheControl(this._rescc) }); |
| 106 | delete this._resHeaders.expires; |
| 107 | delete this._resHeaders.pragma; |
| 108 | } |
| 109 | |
| 110 | // When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive |
| 111 | // as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1). |
| 112 | if (!res.headers['cache-control'] && /no-cache/.test(res.headers.pragma)) { |
| 113 | this._rescc['no-cache'] = true; |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | CachePolicy.prototype.now = function now() { |
| 118 | return Date.now(); |
| 119 | }; |
| 120 | |
| 121 | CachePolicy.prototype.storable = function storable() { |
| 122 | // The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it. |
| 123 | return !!(!this._reqcc['no-store'] && ( |
| 124 | // A cache MUST NOT store a response to any request, unless: |
| 125 | // The request method is understood by the cache and defined as being cacheable, and |
| 126 | 'GET' === this._method || 'HEAD' === this._method || 'POST' === this._method && this._hasExplicitExpiration()) && |
| 127 | // the response status code is understood by the cache, and |
| 128 | understoodStatuses.indexOf(this._status) !== -1 && |
| 129 | // the "no-store" cache directive does not appear in request or response header fields, and |
| 130 | !this._rescc['no-store'] && ( |
| 131 | // the "private" response directive does not appear in the response, if the cache is shared, and |
| 132 | !this._isShared || !this._rescc.private) && ( |
| 133 | // the Authorization header field does not appear in the request, if the cache is shared, |
| 134 | !this._isShared || this._noAuthorization || this._allowsStoringAuthenticated()) && ( |
| 135 | // the response either: |
| 136 | |
| 137 | // contains an Expires header field, or |
| 138 | this._resHeaders.expires || |
| 139 | // contains a max-age response directive, or |
| 140 | // contains a s-maxage response directive and the cache is shared, or |
| 141 | // contains a public response directive. |
| 142 | this._rescc.public || this._rescc['max-age'] || this._rescc['s-maxage'] || |
| 143 | // has a status code that is defined as cacheable by default |
| 144 | statusCodeCacheableByDefault.indexOf(this._status) !== -1)); |
| 145 | }; |
| 146 | |
| 147 | CachePolicy.prototype._hasExplicitExpiration = function _hasExplicitExpiration() { |
| 148 | // 4.2.1 Calculating Freshness Lifetime |
| 149 | return this._isShared && this._rescc['s-maxage'] || this._rescc['max-age'] || this._resHeaders.expires; |
| 150 | }; |
| 151 | |
| 152 | CachePolicy.prototype._assertRequestHasHeaders = function _assertRequestHasHeaders(req) { |
| 153 | if (!req || !req.headers) { |
| 154 | throw Error("Request headers missing"); |
| 155 | } |
| 156 | }; |
| 157 | |
| 158 | CachePolicy.prototype.satisfiesWithoutRevalidation = function satisfiesWithoutRevalidation(req) { |
| 159 | this._assertRequestHasHeaders(req); |
| 160 | |
| 161 | // When presented with a request, a cache MUST NOT reuse a stored response, unless: |
| 162 | // the presented request does not contain the no-cache pragma (Section 5.4), nor the no-cache cache directive, |
| 163 | // unless the stored response is successfully validated (Section 4.3), and |
| 164 | var requestCC = parseCacheControl(req.headers['cache-control']); |
| 165 | if (requestCC['no-cache'] || /no-cache/.test(req.headers.pragma)) { |
| 166 | return false; |
| 167 | } |
| 168 | |
| 169 | if (requestCC['max-age'] && this.age() > requestCC['max-age']) { |
| 170 | return false; |
| 171 | } |
| 172 | |
| 173 | if (requestCC['min-fresh'] && this.timeToLive() < 1000 * requestCC['min-fresh']) { |
| 174 | return false; |
| 175 | } |
| 176 | |
| 177 | // the stored response is either: |
| 178 | // fresh, or allowed to be served stale |
| 179 | if (this.stale()) { |
| 180 | var allowsStale = requestCC['max-stale'] && !this._rescc['must-revalidate'] && (true === requestCC['max-stale'] || requestCC['max-stale'] > this.age() - this.maxAge()); |
| 181 | if (!allowsStale) { |
| 182 | return false; |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | return this._requestMatches(req, false); |
| 187 | }; |
| 188 | |
| 189 | CachePolicy.prototype._requestMatches = function _requestMatches(req, allowHeadMethod) { |
| 190 | // The presented effective request URI and that of the stored response match, and |
| 191 | return (!this._url || this._url === req.url) && this._host === req.headers.host && ( |
| 192 | // the request method associated with the stored response allows it to be used for the presented request, and |
| 193 | !req.method || this._method === req.method || allowHeadMethod && 'HEAD' === req.method) && |
| 194 | // selecting header fields nominated by the stored response (if any) match those presented, and |
| 195 | this._varyMatches(req); |
| 196 | }; |
| 197 | |
| 198 | CachePolicy.prototype._allowsStoringAuthenticated = function _allowsStoringAuthenticated() { |
| 199 | // following Cache-Control response directives (Section 5.2.2) have such an effect: must-revalidate, public, and s-maxage. |
| 200 | return this._rescc['must-revalidate'] || this._rescc.public || this._rescc['s-maxage']; |
| 201 | }; |
| 202 | |
| 203 | CachePolicy.prototype._varyMatches = function _varyMatches(req) { |
| 204 | if (!this._resHeaders.vary) { |
| 205 | return true; |
| 206 | } |
| 207 | |
| 208 | // A Vary header field-value of "*" always fails to match |
| 209 | if (this._resHeaders.vary === '*') { |
| 210 | return false; |
| 211 | } |
| 212 | |
| 213 | var fields = this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/); |
| 214 | for (var _iterator2 = fields, _isArray2 = Array.isArray(_iterator2), _i2 = 0, _iterator2 = _isArray2 ? _iterator2 : _iterator2[Symbol.iterator]();;) { |
| 215 | var _ref3; |
| 216 | |
| 217 | if (_isArray2) { |
| 218 | if (_i2 >= _iterator2.length) break; |
| 219 | _ref3 = _iterator2[_i2++]; |
| 220 | } else { |
| 221 | _i2 = _iterator2.next(); |
| 222 | if (_i2.done) break; |
| 223 | _ref3 = _i2.value; |
| 224 | } |
| 225 | |
| 226 | var name = _ref3; |
| 227 | |
| 228 | if (req.headers[name] !== this._reqHeaders[name]) return false; |
| 229 | } |
| 230 | return true; |
| 231 | }; |
| 232 | |
| 233 | CachePolicy.prototype._copyWithoutHopByHopHeaders = function _copyWithoutHopByHopHeaders(inHeaders) { |
| 234 | var headers = {}; |
| 235 | for (var name in inHeaders) { |
| 236 | if (hopByHopHeaders[name]) continue; |
| 237 | headers[name] = inHeaders[name]; |
| 238 | } |
| 239 | // 9.1. Connection |
| 240 | if (inHeaders.connection) { |
| 241 | var tokens = inHeaders.connection.trim().split(/\s*,\s*/); |
| 242 | for (var _iterator3 = tokens, _isArray3 = Array.isArray(_iterator3), _i3 = 0, _iterator3 = _isArray3 ? _iterator3 : _iterator3[Symbol.iterator]();;) { |
| 243 | var _ref4; |
| 244 | |
| 245 | if (_isArray3) { |
| 246 | if (_i3 >= _iterator3.length) break; |
| 247 | _ref4 = _iterator3[_i3++]; |
| 248 | } else { |
| 249 | _i3 = _iterator3.next(); |
| 250 | if (_i3.done) break; |
| 251 | _ref4 = _i3.value; |
| 252 | } |
| 253 | |
| 254 | var _name = _ref4; |
| 255 | |
| 256 | delete headers[_name]; |
| 257 | } |
| 258 | } |
| 259 | if (headers.warning) { |
| 260 | var warnings = headers.warning.split(/,/).filter(function (warning) { |
| 261 | return !/^\s*1[0-9][0-9]/.test(warning); |
| 262 | }); |
| 263 | if (!warnings.length) { |
| 264 | delete headers.warning; |
| 265 | } else { |
| 266 | headers.warning = warnings.join(',').trim(); |
| 267 | } |
| 268 | } |
| 269 | return headers; |
| 270 | }; |
| 271 | |
| 272 | CachePolicy.prototype.responseHeaders = function responseHeaders() { |
| 273 | var headers = this._copyWithoutHopByHopHeaders(this._resHeaders); |
| 274 | var age = this.age(); |
| 275 | |
| 276 | // A cache SHOULD generate 113 warning if it heuristically chose a freshness |
| 277 | // lifetime greater than 24 hours and the response's age is greater than 24 hours. |
| 278 | if (age > 3600 * 24 && !this._hasExplicitExpiration() && this.maxAge() > 3600 * 24) { |
| 279 | headers.warning = (headers.warning ? `${headers.warning}, ` : '') + '113 - "rfc7234 5.5.4"'; |
| 280 | } |
| 281 | headers.age = `${Math.round(age)}`; |
| 282 | return headers; |
| 283 | }; |
| 284 | |
| 285 | /** |
| 286 | * Value of the Date response header or current time if Date was demed invalid |
| 287 | * @return timestamp |
| 288 | */ |
| 289 | |
| 290 | |
| 291 | CachePolicy.prototype.date = function date() { |
| 292 | var dateValue = Date.parse(this._resHeaders.date); |
| 293 | var maxClockDrift = 8 * 3600 * 1000; |
| 294 | if (Number.isNaN(dateValue) || dateValue < this._responseTime - maxClockDrift || dateValue > this._responseTime + maxClockDrift) { |
| 295 | return this._responseTime; |
| 296 | } |
| 297 | return dateValue; |
| 298 | }; |
| 299 | |
| 300 | /** |
| 301 | * Value of the Age header, in seconds, updated for the current time. |
| 302 | * May be fractional. |
| 303 | * |
| 304 | * @return Number |
| 305 | */ |
| 306 | |
| 307 | |
| 308 | CachePolicy.prototype.age = function age() { |
| 309 | var age = Math.max(0, (this._responseTime - this.date()) / 1000); |
| 310 | if (this._resHeaders.age) { |
| 311 | var ageValue = this._ageValue(); |
| 312 | if (ageValue > age) age = ageValue; |
| 313 | } |
| 314 | |
| 315 | var residentTime = (this.now() - this._responseTime) / 1000; |
| 316 | return age + residentTime; |
| 317 | }; |
| 318 | |
| 319 | CachePolicy.prototype._ageValue = function _ageValue() { |
| 320 | var ageValue = parseInt(this._resHeaders.age); |
| 321 | return isFinite(ageValue) ? ageValue : 0; |
| 322 | }; |
| 323 | |
| 324 | /** |
| 325 | * Value of applicable max-age (or heuristic equivalent) in seconds. This counts since response's `Date`. |
| 326 | * |
| 327 | * For an up-to-date value, see `timeToLive()`. |
| 328 | * |
| 329 | * @return Number |
| 330 | */ |
| 331 | |
| 332 | |
| 333 | CachePolicy.prototype.maxAge = function maxAge() { |
| 334 | if (!this.storable() || this._rescc['no-cache']) { |
| 335 | return 0; |
| 336 | } |
| 337 | |
| 338 | // Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default |
| 339 | // so this implementation requires explicit opt-in via public header |
| 340 | if (this._isShared && this._resHeaders['set-cookie'] && !this._rescc.public && !this._rescc.immutable) { |
| 341 | return 0; |
| 342 | } |
| 343 | |
| 344 | if (this._resHeaders.vary === '*') { |
| 345 | return 0; |
| 346 | } |
| 347 | |
| 348 | if (this._isShared) { |
| 349 | if (this._rescc['proxy-revalidate']) { |
| 350 | return 0; |
| 351 | } |
| 352 | // if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field. |
| 353 | if (this._rescc['s-maxage']) { |
| 354 | return parseInt(this._rescc['s-maxage'], 10); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | // If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field. |
| 359 | if (this._rescc['max-age']) { |
| 360 | return parseInt(this._rescc['max-age'], 10); |
| 361 | } |
| 362 | |
| 363 | var defaultMinTtl = this._rescc.immutable ? this._immutableMinTtl : 0; |
| 364 | |
| 365 | var dateValue = this.date(); |
| 366 | if (this._resHeaders.expires) { |
| 367 | var expires = Date.parse(this._resHeaders.expires); |
| 368 | // A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired"). |
| 369 | if (Number.isNaN(expires) || expires < dateValue) { |
| 370 | return 0; |
| 371 | } |
| 372 | return Math.max(defaultMinTtl, (expires - dateValue) / 1000); |
| 373 | } |
| 374 | |
| 375 | if (this._resHeaders['last-modified']) { |
| 376 | var lastModified = Date.parse(this._resHeaders['last-modified']); |
| 377 | if (isFinite(lastModified) && dateValue > lastModified) { |
| 378 | return Math.max(defaultMinTtl, (dateValue - lastModified) / 1000 * this._cacheHeuristic); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | return defaultMinTtl; |
| 383 | }; |
| 384 | |
| 385 | CachePolicy.prototype.timeToLive = function timeToLive() { |
| 386 | return Math.max(0, this.maxAge() - this.age()) * 1000; |
| 387 | }; |
| 388 | |
| 389 | CachePolicy.prototype.stale = function stale() { |
| 390 | return this.maxAge() <= this.age(); |
| 391 | }; |
| 392 | |
| 393 | CachePolicy.fromObject = function fromObject(obj) { |
| 394 | return new this(undefined, undefined, { _fromObject: obj }); |
| 395 | }; |
| 396 | |
| 397 | CachePolicy.prototype._fromObject = function _fromObject(obj) { |
| 398 | if (this._responseTime) throw Error("Reinitialized"); |
| 399 | if (!obj || obj.v !== 1) throw Error("Invalid serialization"); |
| 400 | |
| 401 | this._responseTime = obj.t; |
| 402 | this._isShared = obj.sh; |
| 403 | this._cacheHeuristic = obj.ch; |
| 404 | this._immutableMinTtl = obj.imm !== undefined ? obj.imm : 24 * 3600 * 1000; |
| 405 | this._status = obj.st; |
| 406 | this._resHeaders = obj.resh; |
| 407 | this._rescc = obj.rescc; |
| 408 | this._method = obj.m; |
| 409 | this._url = obj.u; |
| 410 | this._host = obj.h; |
| 411 | this._noAuthorization = obj.a; |
| 412 | this._reqHeaders = obj.reqh; |
| 413 | this._reqcc = obj.reqcc; |
| 414 | }; |
| 415 | |
| 416 | CachePolicy.prototype.toObject = function toObject() { |
| 417 | return { |
| 418 | v: 1, |
| 419 | t: this._responseTime, |
| 420 | sh: this._isShared, |
| 421 | ch: this._cacheHeuristic, |
| 422 | imm: this._immutableMinTtl, |
| 423 | st: this._status, |
| 424 | resh: this._resHeaders, |
| 425 | rescc: this._rescc, |
| 426 | m: this._method, |
| 427 | u: this._url, |
| 428 | h: this._host, |
| 429 | a: this._noAuthorization, |
| 430 | reqh: this._reqHeaders, |
| 431 | reqcc: this._reqcc |
| 432 | }; |
| 433 | }; |
| 434 | |
| 435 | /** |
| 436 | * Headers for sending to the origin server to revalidate stale response. |
| 437 | * Allows server to return 304 to allow reuse of the previous response. |
| 438 | * |
| 439 | * Hop by hop headers are always stripped. |
| 440 | * Revalidation headers may be added or removed, depending on request. |
| 441 | */ |
| 442 | |
| 443 | |
| 444 | CachePolicy.prototype.revalidationHeaders = function revalidationHeaders(incomingReq) { |
| 445 | this._assertRequestHasHeaders(incomingReq); |
| 446 | var headers = this._copyWithoutHopByHopHeaders(incomingReq.headers); |
| 447 | |
| 448 | // This implementation does not understand range requests |
| 449 | delete headers['if-range']; |
| 450 | |
| 451 | if (!this._requestMatches(incomingReq, true) || !this.storable()) { |
| 452 | // revalidation allowed via HEAD |
| 453 | // not for the same resource, or wasn't allowed to be cached anyway |
| 454 | delete headers['if-none-match']; |
| 455 | delete headers['if-modified-since']; |
| 456 | return headers; |
| 457 | } |
| 458 | |
| 459 | /* MUST send that entity-tag in any cache validation request (using If-Match or If-None-Match) if an entity-tag has been provided by the origin server. */ |
| 460 | if (this._resHeaders.etag) { |
| 461 | headers['if-none-match'] = headers['if-none-match'] ? `${headers['if-none-match']}, ${this._resHeaders.etag}` : this._resHeaders.etag; |
| 462 | } |
| 463 | |
| 464 | // Clients MAY issue simple (non-subrange) GET requests with either weak validators or strong validators. Clients MUST NOT use weak validators in other forms of request. |
| 465 | var forbidsWeakValidators = headers['accept-ranges'] || headers['if-match'] || headers['if-unmodified-since'] || this._method && this._method != 'GET'; |
| 466 | |
| 467 | /* SHOULD send the Last-Modified value in non-subrange cache validation requests (using If-Modified-Since) if only a Last-Modified value has been provided by the origin server. |
| 468 | Note: This implementation does not understand partial responses (206) */ |
| 469 | if (forbidsWeakValidators) { |
| 470 | delete headers['if-modified-since']; |
| 471 | |
| 472 | if (headers['if-none-match']) { |
| 473 | var etags = headers['if-none-match'].split(/,/).filter(function (etag) { |
| 474 | return !/^\s*W\//.test(etag); |
| 475 | }); |
| 476 | if (!etags.length) { |
| 477 | delete headers['if-none-match']; |
| 478 | } else { |
| 479 | headers['if-none-match'] = etags.join(',').trim(); |
| 480 | } |
| 481 | } |
| 482 | } else if (this._resHeaders['last-modified'] && !headers['if-modified-since']) { |
| 483 | headers['if-modified-since'] = this._resHeaders['last-modified']; |
| 484 | } |
| 485 | |
| 486 | return headers; |
| 487 | }; |
| 488 | |
| 489 | /** |
| 490 | * Creates new CachePolicy with information combined from the previews response, |
| 491 | * and the new revalidation response. |
| 492 | * |
| 493 | * Returns {policy, modified} where modified is a boolean indicating |
| 494 | * whether the response body has been modified, and old cached body can't be used. |
| 495 | * |
| 496 | * @return {Object} {policy: CachePolicy, modified: Boolean} |
| 497 | */ |
| 498 | |
| 499 | |
| 500 | CachePolicy.prototype.revalidatedPolicy = function revalidatedPolicy(request, response) { |
| 501 | this._assertRequestHasHeaders(request); |
| 502 | if (!response || !response.headers) { |
| 503 | throw Error("Response headers missing"); |
| 504 | } |
| 505 | |
| 506 | // These aren't going to be supported exactly, since one CachePolicy object |
| 507 | // doesn't know about all the other cached objects. |
| 508 | var matches = false; |
| 509 | if (response.status !== undefined && response.status != 304) { |
| 510 | matches = false; |
| 511 | } else if (response.headers.etag && !/^\s*W\//.test(response.headers.etag)) { |
| 512 | // "All of the stored responses with the same strong validator are selected. |
| 513 | // If none of the stored responses contain the same strong validator, |
| 514 | // then the cache MUST NOT use the new response to update any stored responses." |
| 515 | matches = this._resHeaders.etag && this._resHeaders.etag.replace(/^\s*W\//, '') === response.headers.etag; |
| 516 | } else if (this._resHeaders.etag && response.headers.etag) { |
| 517 | // "If the new response contains a weak validator and that validator corresponds |
| 518 | // to one of the cache's stored responses, |
| 519 | // then the most recent of those matching stored responses is selected for update." |
| 520 | matches = this._resHeaders.etag.replace(/^\s*W\//, '') === response.headers.etag.replace(/^\s*W\//, ''); |
| 521 | } else if (this._resHeaders['last-modified']) { |
| 522 | matches = this._resHeaders['last-modified'] === response.headers['last-modified']; |
| 523 | } else { |
| 524 | // If the new response does not include any form of validator (such as in the case where |
| 525 | // a client generates an If-Modified-Since request from a source other than the Last-Modified |
| 526 | // response header field), and there is only one stored response, and that stored response also |
| 527 | // lacks a validator, then that stored response is selected for update. |
| 528 | if (!this._resHeaders.etag && !this._resHeaders['last-modified'] && !response.headers.etag && !response.headers['last-modified']) { |
| 529 | matches = true; |
| 530 | } |
| 531 | } |
| 532 | |
| 533 | if (!matches) { |
| 534 | return { |
| 535 | policy: new this.constructor(request, response), |
| 536 | modified: true |
| 537 | }; |
| 538 | } |
| 539 | |
| 540 | // use other header fields provided in the 304 (Not Modified) response to replace all instances |
| 541 | // of the corresponding header fields in the stored response. |
| 542 | var headers = {}; |
| 543 | for (var k in this._resHeaders) { |
| 544 | headers[k] = k in response.headers && !excludedFromRevalidationUpdate[k] ? response.headers[k] : this._resHeaders[k]; |
| 545 | } |
| 546 | |
| 547 | var newResponse = Object.assign({}, response, { |
| 548 | status: this._status, |
| 549 | method: this._method, |
| 550 | headers |
| 551 | }); |
| 552 | return { |
| 553 | policy: new this.constructor(request, newResponse), |
| 554 | modified: false |
| 555 | }; |
| 556 | }; |
| 557 | |
| 558 | return CachePolicy; |
| 559 | }(); |