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}