PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / mrclay / minify / lib / HTTP / ConditionalGet.php

ConditionalGet.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at vendor/mrclay/minify/lib/HTTP/ConditionalGet.php

377 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class HTTP_ConditionalGet
4 * @package Minify
5 * @subpackage HTTP
6 */
7
8 /**
9 * Implement conditional GET via a timestamp or hash of content
10 *
11 * E.g. Content from DB with update time:
12 * <code>
13 * list($updateTime, $content) = getDbUpdateAndContent();
14 * $cg = new HTTP_ConditionalGet(array(
15 * 'lastModifiedTime' => $updateTime
16 * ,'isPublic' => true
17 * ));
18 * $cg->sendHeaders();
19 * if ($cg->cacheIsValid) {
20 * exit();
21 * }
22 * echo $content;
23 * </code>
24 *
25 * E.g. Shortcut for the above
26 * <code>
27 * HTTP_ConditionalGet::check($updateTime, true); // exits if client has cache
28 * echo $content;
29 * </code>
30 *
31 * E.g. Content from DB with no update time:
32 * <code>
33 * $content = getContentFromDB();
34 * $cg = new HTTP_ConditionalGet(array(
35 * 'contentHash' => md5($content)
36 * ));
37 * $cg->sendHeaders();
38 * if ($cg->cacheIsValid) {
39 * exit();
40 * }
41 * echo $content;
42 * </code>
43 *
44 * E.g. Static content with some static includes:
45 * <code>
46 * // before content
47 * $cg = new HTTP_ConditionalGet(array(
48 * 'lastUpdateTime' => max(
49 * filemtime(__FILE__)
50 * ,filemtime('/path/to/header.inc')
51 * ,filemtime('/path/to/footer.inc')
52 * )
53 * ));
54 * $cg->sendHeaders();
55 * if ($cg->cacheIsValid) {
56 * exit();
57 * }
58 * </code>
59 * @package Minify
60 * @subpackage HTTP
61 * @author Stephen Clay <[email protected]>
62 */
63 class HTTP_ConditionalGet
64 {
65
66 /**
67 * Does the client have a valid copy of the requested resource?
68 *
69 * You'll want to check this after instantiating the object. If true, do
70 * not send content, just call sendHeaders() if you haven't already.
71 *
72 * @var bool
73 */
74 public $cacheIsValid = null;
75
76 /**
77 * @param array $spec options
78 *
79 * 'isPublic': (bool) if false, the Cache-Control header will contain
80 * "private", allowing only browser caching. (default false)
81 *
82 * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers
83 * will be sent with content. This is recommended.
84 *
85 * 'encoding': (string) if set, the header "Vary: Accept-Encoding" will
86 * always be sent and a truncated version of the encoding will be appended
87 * to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient
88 * checking of the client's If-None-Match header, as the encoding portion of
89 * the ETag will be stripped before comparison.
90 *
91 * 'contentHash': (string) if given, only the ETag header can be sent with
92 * content (only HTTP1.1 clients can conditionally GET). The given string
93 * should be short with no quote characters and always change when the
94 * resource changes (recommend md5()). This is not needed/used if
95 * lastModifiedTime is given.
96 *
97 * 'eTag': (string) if given, this will be used as the ETag header rather
98 * than values based on lastModifiedTime or contentHash. Also the encoding
99 * string will not be appended to the given value as described above.
100 *
101 * 'invalidate': (bool) if true, the client cache will be considered invalid
102 * without testing. Effectively this disables conditional GET.
103 * (default false)
104 *
105 * 'maxAge': (int) if given, this will set the Cache-Control max-age in
106 * seconds, and also set the Expires header to the equivalent GMT date.
107 * After the max-age period has passed, the browser will again send a
108 * conditional GET to revalidate its cache.
109 */
110 public function __construct($spec)
111 {
112 $scope = (isset($spec['isPublic']) && $spec['isPublic'])
113 ? 'public'
114 : 'private';
115 $maxAge = 0;
116 // backwards compatibility (can be removed later)
117 if (isset($spec['setExpires'])
118 && is_numeric($spec['setExpires'])
119 && ! isset($spec['maxAge'])) {
120 $spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME'];
121 }
122 if (isset($spec['maxAge'])) {
123 $maxAge = $spec['maxAge'];
124 $this->_headers['Expires'] = self::gmtDate(
125 $_SERVER['REQUEST_TIME'] + $spec['maxAge']
126 );
127 }
128 $etagAppend = '';
129 if (isset($spec['encoding'])) {
130 $this->_stripEtag = true;
131 if ('' !== $spec['encoding']) {
132 $this->_headers['Vary'] = 'Accept-Encoding';
133 if (0 === strpos($spec['encoding'], 'x-')) {
134 $spec['encoding'] = substr($spec['encoding'], 2);
135 }
136 $etagAppend = ';' . substr($spec['encoding'], 0, 2);
137 }
138 }
139 if (isset($spec['lastModifiedTime'])) {
140 $this->_setLastModified($spec['lastModifiedTime']);
141 if (isset($spec['eTag'])) { // Use it
142 $this->_setEtag($spec['eTag'], $scope);
143 } else { // base both headers on time
144 $this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope);
145 }
146 } elseif (isset($spec['eTag'])) { // Use it
147 $this->_setEtag($spec['eTag'], $scope);
148 } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag
149 $this->_setEtag($spec['contentHash'] . $etagAppend, $scope);
150 }
151 $privacy = ($scope === 'private')
152 ? ', private'
153 : '';
154 $this->_headers['Cache-Control'] = "max-age={$maxAge}{$privacy}";
155 // invalidate cache if disabled, otherwise check
156 $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate'])
157 ? false
158 : $this->_isCacheValid();
159 }
160
161 /**
162 * Get array of output headers to be sent
163 *
164 * In the case of 304 responses, this array will only contain the response
165 * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified')
166 *
167 * Otherwise something like:
168 * <code>
169 * array(
170 * 'Cache-Control' => 'max-age=0, public'
171 * ,'ETag' => '"foobar"'
172 * )
173 * </code>
174 *
175 * @return array
176 */
177 public function getHeaders()
178 {
179 return $this->_headers;
180 }
181
182 /**
183 * Set the Content-Length header in bytes
184 *
185 * With most PHP configs, as long as you don't flush() output, this method
186 * is not needed and PHP will buffer all output and set Content-Length for
187 * you. Otherwise you'll want to call this to let the client know up front.
188 *
189 * @param int $bytes
190 *
191 * @return int copy of input $bytes
192 */
193 public function setContentLength($bytes)
194 {
195 return $this->_headers['Content-Length'] = $bytes;
196 }
197
198 /**
199 * Send headers
200 *
201 * @see getHeaders()
202 *
203 * Note this doesn't "clear" the headers. Calling sendHeaders() will
204 * call header() again (but probably have not effect) and getHeaders() will
205 * still return the headers.
206 *
207 * @return null
208 */
209 public function sendHeaders()
210 {
211 $headers = $this->_headers;
212 if (array_key_exists('_responseCode', $headers)) {
213 // FastCGI environments require 3rd arg to header() to be set
214 list(, $code) = explode(' ', $headers['_responseCode'], 3);
215 header($headers['_responseCode'], true, $code);
216 unset($headers['_responseCode']);
217 }
218 foreach ($headers as $name => $val) {
219 header($name . ': ' . $val);
220 }
221 }
222
223 /**
224 * Exit if the client's cache is valid for this resource
225 *
226 * This is a convenience method for common use of the class
227 *
228 * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers
229 * will be sent with content. This is recommended.
230 *
231 * @param bool $isPublic (default false) if true, the Cache-Control header
232 * will contain "public", allowing proxies to cache the content. Otherwise
233 * "private" will be sent, allowing only browser caching.
234 *
235 * @param array $options (default empty) additional options for constructor
236 */
237 public static function check($lastModifiedTime = null, $isPublic = false, $options = array())
238 {
239 if (null !== $lastModifiedTime) {
240 $options['lastModifiedTime'] = (int)$lastModifiedTime;
241 }
242 $options['isPublic'] = (bool)$isPublic;
243 $cg = new HTTP_ConditionalGet($options);
244 $cg->sendHeaders();
245 if ($cg->cacheIsValid) {
246 exit();
247 }
248 }
249
250 /**
251 * Get a GMT formatted date for use in HTTP headers
252 *
253 * <code>
254 * header('Expires: ' . HTTP_ConditionalGet::gmtdate($time));
255 * </code>
256 *
257 * @param int $time unix timestamp
258 *
259 * @return string
260 */
261 public static function gmtDate($time)
262 {
263 return gmdate('D, d M Y H:i:s \G\M\T', $time);
264 }
265
266 protected $_headers = array();
267 protected $_lmTime = null;
268 protected $_etag = null;
269 protected $_stripEtag = false;
270
271 /**
272 * @param string $hash
273 *
274 * @param string $scope
275 */
276 protected function _setEtag($hash, $scope)
277 {
278 $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"';
279 $this->_headers['ETag'] = $this->_etag;
280 }
281
282 /**
283 * @param int $time
284 */
285 protected function _setLastModified($time)
286 {
287 $this->_lmTime = (int)$time;
288 $this->_headers['Last-Modified'] = self::gmtDate($time);
289 }
290
291 /**
292 * Determine validity of client cache and queue 304 header if valid
293 *
294 * @return bool
295 */
296 protected function _isCacheValid()
297 {
298 if (null === $this->_etag) {
299 // lmTime is copied to ETag, so this condition implies that the
300 // server sent neither ETag nor Last-Modified, so the client can't
301 // possibly has a valid cache.
302 return false;
303 }
304 $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified());
305 if ($isValid) {
306 $this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified';
307 }
308
309 return $isValid;
310 }
311
312 /**
313 * @return bool
314 */
315 protected function resourceMatchedEtag()
316 {
317 if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
318 return false;
319 }
320 $clientEtagList = PHP_VERSION_ID < 50400 && get_magic_quotes_gpc()
321 ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH'])
322 : $_SERVER['HTTP_IF_NONE_MATCH'];
323 $clientEtags = explode(',', $clientEtagList);
324
325 $compareTo = $this->normalizeEtag($this->_etag);
326 foreach ($clientEtags as $clientEtag) {
327 if ($this->normalizeEtag($clientEtag) === $compareTo) {
328 // respond with the client's matched ETag, even if it's not what
329 // we would've sent by default
330 $this->_headers['ETag'] = trim($clientEtag);
331
332 return true;
333 }
334 }
335
336 return false;
337 }
338
339 /**
340 * @param string $etag
341 *
342 * @return string
343 */
344 protected function normalizeEtag($etag)
345 {
346 $etag = trim($etag);
347
348 return $this->_stripEtag
349 ? preg_replace('/;\\w\\w"$/', '"', $etag)
350 : $etag;
351 }
352
353 /**
354 * @return bool
355 */
356 protected function resourceNotModified()
357 {
358 if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
359 return false;
360 }
361 // strip off IE's extra data (semicolon)
362 list($ifModifiedSince) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'], 2);
363
364 $date = new DateTime($ifModifiedSince, new DateTimeZone('UTC'));
365
366 if ($date->getTimestamp() >= $this->_lmTime) {
367 // Apache 2.2's behavior. If there was no ETag match, send the
368 // non-encoded version of the ETag value.
369 $this->_headers['ETag'] = $this->normalizeEtag($this->_etag);
370
371 return true;
372 }
373
374 return false;
375 }
376 }
377