1<?php
2/*
3 * Copyright 2012 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17/**
18 * Implement the caching directives specified in rfc2616. This
19 * implementation is guided by the guidance offered in rfc2616-sec13.
20 * @author Chirag Shah <chirags@google.com>
21 */
22class Google_CacheParser {
23  public static $CACHEABLE_HTTP_METHODS = array('GET', 'HEAD');
24  public static $CACHEABLE_STATUS_CODES = array('200', '203', '300', '301');
25
26  private function __construct() {}
27
28  /**
29   * Check if an HTTP request can be cached by a private local cache.
30   *
31   * @static
32   * @param Google_HttpRequest $resp
33   * @return bool True if the request is cacheable.
34   * False if the request is uncacheable.
35   */
36  public static function isRequestCacheable (Google_HttpRequest $resp) {
37    $method = $resp->getRequestMethod();
38    if (! in_array($method, self::$CACHEABLE_HTTP_METHODS)) {
39      return false;
40    }
41
42    // Don't cache authorized requests/responses.
43    // [rfc2616-14.8] When a shared cache receives a request containing an
44    // Authorization field, it MUST NOT return the corresponding response
45    // as a reply to any other request...
46    if ($resp->getRequestHeader("authorization")) {
47      return false;
48    }
49
50    return true;
51  }
52
53  /**
54   * Check if an HTTP response can be cached by a private local cache.
55   *
56   * @static
57   * @param Google_HttpRequest $resp
58   * @return bool True if the response is cacheable.
59   * False if the response is un-cacheable.
60   */
61  public static function isResponseCacheable (Google_HttpRequest $resp) {
62    // First, check if the HTTP request was cacheable before inspecting the
63    // HTTP response.
64    if (false == self::isRequestCacheable($resp)) {
65      return false;
66    }
67
68    $code = $resp->getResponseHttpCode();
69    if (! in_array($code, self::$CACHEABLE_STATUS_CODES)) {
70      return false;
71    }
72
73    // The resource is uncacheable if the resource is already expired and
74    // the resource doesn't have an ETag for revalidation.
75    $etag = $resp->getResponseHeader("etag");
76    if (self::isExpired($resp) && $etag == false) {
77      return false;
78    }
79
80    // [rfc2616-14.9.2]  If [no-store is] sent in a response, a cache MUST NOT
81    // store any part of either this response or the request that elicited it.
82    $cacheControl = $resp->getParsedCacheControl();
83    if (isset($cacheControl['no-store'])) {
84      return false;
85    }
86
87    // Pragma: no-cache is an http request directive, but is occasionally
88    // used as a response header incorrectly.
89    $pragma = $resp->getResponseHeader('pragma');
90    if ($pragma == 'no-cache' || strpos($pragma, 'no-cache') !== false) {
91      return false;
92    }
93
94    // [rfc2616-14.44] Vary: * is extremely difficult to cache. "It implies that
95    // a cache cannot determine from the request headers of a subsequent request
96    // whether this response is the appropriate representation."
97    // Given this, we deem responses with the Vary header as uncacheable.
98    $vary = $resp->getResponseHeader('vary');
99    if ($vary) {
100      return false;
101    }
102
103    return true;
104  }
105
106  /**
107   * @static
108   * @param Google_HttpRequest $resp
109   * @return bool True if the HTTP response is considered to be expired.
110   * False if it is considered to be fresh.
111   */
112  public static function isExpired(Google_HttpRequest $resp) {
113    // HTTP/1.1 clients and caches MUST treat other invalid date formats,
114    // especially including the value “0”, as in the past.
115    $parsedExpires = false;
116    $responseHeaders = $resp->getResponseHeaders();
117    if (isset($responseHeaders['expires'])) {
118      $rawExpires = $responseHeaders['expires'];
119      // Check for a malformed expires header first.
120      if (empty($rawExpires) || (is_numeric($rawExpires) && $rawExpires <= 0)) {
121        return true;
122      }
123
124      // See if we can parse the expires header.
125      $parsedExpires = strtotime($rawExpires);
126      if (false == $parsedExpires || $parsedExpires <= 0) {
127        return true;
128      }
129    }
130
131    // Calculate the freshness of an http response.
132    $freshnessLifetime = false;
133    $cacheControl = $resp->getParsedCacheControl();
134    if (isset($cacheControl['max-age'])) {
135      $freshnessLifetime = $cacheControl['max-age'];
136    }
137
138    $rawDate = $resp->getResponseHeader('date');
139    $parsedDate = strtotime($rawDate);
140
141    if (empty($rawDate) || false == $parsedDate) {
142      $parsedDate = time();
143    }
144    if (false == $freshnessLifetime && isset($responseHeaders['expires'])) {
145      $freshnessLifetime = $parsedExpires - $parsedDate;
146    }
147
148    if (false == $freshnessLifetime) {
149      return true;
150    }
151
152    // Calculate the age of an http response.
153    $age = max(0, time() - $parsedDate);
154    if (isset($responseHeaders['age'])) {
155      $age = max($age, strtotime($responseHeaders['age']));
156    }
157
158    return $freshnessLifetime <= $age;
159  }
160
161  /**
162   * Determine if a cache entry should be revalidated with by the origin.
163   *
164   * @param Google_HttpRequest $response
165   * @return bool True if the entry is expired, else return false.
166   */
167  public static function mustRevalidate(Google_HttpRequest $response) {
168    // [13.3] When a cache has a stale entry that it would like to use as a
169    // response to a client's request, it first has to check with the origin
170    // server to see if its cached entry is still usable.
171    return self::isExpired($response);
172  }
173}