HttpCaching.php

00001 <?php
00013 class HttpCaching {
00014 
00019   private $cacheControlDirectives = array();
00023   private $ages = array('max-age' => -1, 's-maxage' => -1);
00027   private $lastModified;
00028   private $eTag;
00029 
00044   function sendStatusAndHeaders($die) {
00045     $isFresh = $_SERVER['REQUEST_METHOD'] == "GET" ? $this->isFresh() : false;
00046     // Send back a 304?
00047     if ($isFresh == true) {
00048       header('HTTP/1.1 304 Not Modified');
00049     }
00050     $this->sendHeaders();
00051     // Die if 304?
00052     if ($isFresh == true && $die == true) {
00053       exit();
00054     }
00055     return $isFresh;
00056   }
00057 
00069   function sendHeaders() {
00070     // Expires corresponds to max-age
00071     if ($this->ages['max-age'] >= 0) {
00072       header('Expires: ' . self::formatDate(time() + $this->ages['max-age']), 1);
00073     }
00074     // Cache-Control
00075     if (!is_array($this->cacheControlDirectives)) {
00076       $this->cacheControlDirectives = array();
00077     }
00078     foreach($this->ages as $dir => $value) {
00079       if ($value >= 0) {
00080         array_push($this->cacheControlDirectives, "$dir=$value");
00081       }
00082     }
00083     if (count($this->cacheControlDirectives) > 0) {
00084       header('Cache-Control: ' .
00085              implode(', ', $this->cacheControlDirectives), 1);
00086     }
00087     // At least one of ETags of Last-Modified will be sent for cacheability
00088     if ($this->eTag) {
00089       header('ETag: ' . $this->eTag);
00090     }
00091     if ($this->lastModified) {
00092       $lm = $this->lastModified;
00093     } else if (!$this->eTag) {
00094       $lm = time();
00095     }
00096     if ($lm) {
00097       header('Last-Modified: ' . self::formatDate($lm));
00098     }
00099   }
00100 
00112   function isFresh() {
00113     if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) &&
00114         !isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
00115       // No information provided by the client
00116       return false;
00117     }
00118 
00119     if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
00120       if (!$this->lastModified) {
00121         return false;
00122       }
00123       // Split the If-Modified-Since (Netscape < v6 gets this wrong)
00124       $ifModifiedSince = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
00125       // Turn the client request If-Modified-Since into a timestamp
00126       $ifModifiedSince = strtotime($ifModifiedSince[0]);
00127       // Compare timestamps (FIXME: make this test '!='?)
00128       if ($this->lastModified > $ifModifiedSince) {
00129         return false;
00130       }
00131     }
00132 
00133     if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
00134       if ($_SERVER['HTTP_IF_NONE_MATCH'] == '*') {
00135         return true;
00136       }
00137       if (!$this->eTag) {
00138         return false;
00139       }
00140       $etags = preg_split('/,\s*/', $_SERVER['HTTP_IF_NONE_MATCH']);
00141       foreach($etags as $e) {
00142         if ($this->etagMatch($e)) {
00143           return true;
00144         }
00145       }
00146       return false;
00147     }
00148 
00149     return true;
00150   }
00151 
00157   function etagMatch($etag) {
00158     if (!$this->eTag) {
00159       return false;
00160     }
00161     if ((self::isEtagWeak($this->eTag) || self::isEtagWeak($etag))
00162         &&
00163         ($_SERVER['REQUEST_METHOD'] != "GET" || isset($_SERVER['HTTP_RANGE'])))
00164     {
00165       // Weak validation only works for non-subrange GET requests
00166       return false;
00167     }
00168     if (self::etagValidator($this->eTag) == self::etagValidator($etag)) {
00169       return true;
00170     } else {
00171       return false;
00172     }
00173   }
00174 
00180   static function isEtagWeak($etag) {
00181     return (substr_compare($etag, 'W/', 0, 2) == 0);
00182   }
00183 
00190   static function etagValidator($etag) {
00191     if (self::isEtagWeak($etag)) {
00192       return substr($etag, 2);
00193     } else {
00194       return $etag;
00195     }
00196   }
00197 
00205   function getDuration($type) {
00206     if ($type != "max-age" && $type != "s-maxage") {
00207       throw new Exception("Invalid type");
00208     }
00209     return $this->ages[$type];
00210   }
00211 
00220   function setDuration($type, $time) {
00221     if ($type != "max-age" && $type != "s-maxage") {
00222       throw new Exception("Invalid type");
00223     }
00224     if (is_numeric($time)) {
00225       $time = intval($time);
00226       if ($time < 0) {
00227         $time = -1;
00228       }
00229     } else {
00230       if ($time == 'now') {
00231         $time = 0;
00232       } else {
00233         $time = strtotime($time) - time();
00234       }
00235       if ($time < 0) {
00236         throw new Exception("Bad interval specified to strtotime()");
00237       }
00238     }
00239     return $this->ages[$type] = $time;
00240   }
00241 
00249   function freshFor($time) {
00250     return $this->setDuration('max-age', $time);
00251   }
00252 
00258   function setCacheControlDirective($type, $set) {
00259     if ($set == true) {
00260       if (!in_array($type, $this->cacheControlDirectives)) {
00261         array_push($this->cacheControlDirectives, $type);
00262       }
00263     } else {
00264       $this->cacheControlDirectives = array_diff($this->cacheControlDirectives,
00265                                                  array($type));
00266     }
00267   }
00268 
00273   function ccPublic($set) {
00274     $this->setCacheControlDirective('public', $set);
00275   }
00276 
00281   function ccNoCache($set) {
00282     $this->setCacheControlDirective('no-cache', $set);
00283   }
00284 
00289   function ccNoStore($set) {
00290     $this->setCacheControlDirective('no-store', $set);
00291   }
00292 
00297   function ccMustRevalidate($set) {
00298     $this->setCacheControlDirective('must-revalidate', $set);
00299   }
00300 
00305   function ccProxyRevalidate($set) {
00306     $this->setCacheControlDirective('proxy-revalidate', $set);
00307   }
00308 
00315   function etag($value) {
00316     $this->eTag = '"' . $value . '"';
00317   }
00318 
00325   function weakEtag($value) {
00326     $this->etag($value);
00327     $this->eTag = "W/" . $this->eTag;
00328   }
00329 
00334   function lastModified($value) {
00335     $this->lastModified = $value;
00336   }
00337 
00343   function setLastModifiedFromFile($file) {
00344     $finfo = stat($file);
00345     if (!$finfo) {
00346       return;
00347     }
00348     $this->lastModified($finfo['mtime']);
00349   }
00350 
00357   static function sendHeadersSpecifyingFreshness($time) {
00358     $hc = new HttpCaching();
00359     $hc->freshFor($time);
00360     $hc->ccMustRevalidate(true);
00361     $hc->sendHeaders();
00362   }
00363 
00369   static function formatDate($time) {
00370     return gmdate("D, d M Y H:i:s", $time) . ' GMT';
00371   }
00372 }
00373 
00403 ?>

PHP HTTP Caching
Hugo Haas