Source for file Response.php

Documentation is available at Response.php

  1. <?php
  2. // vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 fdm=marker encoding=utf8 :
  3. /**
  4.  * Pxxo - build self-supported and interoperable Web graphical components
  5.  * 
  6.  * Copyright (c) 2008, Nicolas Thouvenin
  7.  *
  8.  * All rights reserved.
  9.  *
  10.  * Redistribution and use in source and binary forms, with or without
  11.  * modification, are permitted provided that the following conditions are met:
  12.  *
  13.  *     * Redistributions of source code must retain the above copyright
  14.  *       notice, this list of conditions and the following disclaimer.
  15.  *     * Redistributions in binary form must reproduce the above copyright
  16.  *       notice, this list of conditions and the following disclaimer in the
  17.  *       documentation and/or other materials provided with the distribution.
  18.  *     * Neither the name of the author nor the names of its contributors may be
  19.  *       used to endorse or promote products derived from this software without
  20.  *       specific prior written permission.
  21.  *
  22.  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
  23.  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  24.  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  25.  * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
  26.  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  27.  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  28.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  29.  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  30.  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  31.  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32.  
  33.  * @package    Pxxo
  34.  * @copyright  Copyright (c) 2008 Nicolas Thouvenin
  35.  * @license    http://opensource.org/licenses/bsd-license.php
  36.  * @version    $Id$
  37.  */
  38.  
  39.  
  40. /**
  41.  * Classe de Gestion de l'entete HTTP
  42.  *
  43.  * Cette classe reprend la classe de Hugo Haas (http://larve.net/2006/08/php-http-caching/)
  44.  * et lui ajoute quelques bricoles
  45.  *
  46.  * @package    Pxxo
  47.  * @copyright  Copyright (c) 2008 Nicolas Thouvenin
  48.  * @license    http://opensource.org/licenses/bsd-license.php
  49.  */
  50. {
  51.     /**
  52.      * Array containing the list of Cache-Control directives, except max-age and s-maxage
  53.      * @var array 
  54.      */
  55.     private $_CacheDirectives = array();
  56.  
  57.     /**
  58.      * allowed Cache Control Directives
  59.      * @var array 
  60.      */
  61.     private $CacheDirectives = array(
  62.         'public',
  63.         'private',
  64.         'no-cache',
  65.         'no-store',
  66.         'must-revalidate',
  67.         'proxy-revalidate',
  68.     );
  69.  
  70.     /**
  71.      * Array containing the list of Cache-Control directives, except max-age and s-maxage
  72.      * @var array 
  73.      */
  74.     private $_ContentDirectives = array();
  75.  
  76.     /**
  77.      * allowed Content Directives
  78.      * @var array 
  79.      */
  80.     private $ContentDirectives = array(
  81.         'type',
  82.         'language',
  83.         'disposition',
  84.         'transfer-encoding',
  85.         'length',
  86.     );
  87.  
  88.  
  89.     /**
  90.      * var array Ages: max-age and s-maxage
  91.      */
  92.     private $ages = array(
  93.         'max-age' => -1
  94.         's-maxage' => -1
  95.     );
  96.  
  97.     /**
  98.      * @var string Last-Modified and ETags
  99.      */
  100.     private $lastModified;
  101.  
  102.     /**
  103.      * @var string 
  104.      */
  105.     private $eTag;
  106.  
  107.     /**
  108.      * @var array URL et Status Code
  109.      */
  110.     private $redirection  =array();
  111.  
  112.     /**
  113.      * @var array cookies will be send
  114.      */
  115.     private $cookies = array();
  116.  
  117.     /**
  118.      * Send HTTP caching headers (Expires, Cache-Control, ETags and
  119.      * Last-Modified), and returns a 304 if the client has a cached version.
  120.      *
  121.      * This function returns a 304 if the client already has the latest version
  122.      * of the resource's representation. The PHP
  123.      * script will subsequently quit if $die is set to true.
  124.      *
  125.      * @param $die if set to true, the program terminates if a 304 is being sent, and if set to false, the call will return and the execution will continue
  126.      */
  127.     function sendStatusAndHeaders($die
  128.     {
  129.         if ($this->isRedirect()) {
  130.             header('HTTP/1.1 ' $this->redirection[1]);
  131.             $this->sendHeaders();
  132.             if ($die == trueexit();
  133.         }
  134.  
  135.         $isFresh $_SERVER['REQUEST_METHOD'== "GET" $this->isFresh(false;
  136.         // Send back a 304?
  137.         if ($isFresh == true{
  138.             header('HTTP/1.1 304 Not Modified');
  139.         }
  140.         $this->sendHeaders();
  141.         // Die if 304?
  142.         if ($isFresh == true && $die == trueexit();
  143.     }
  144.  
  145.     /**
  146.      * Send HTTP caching headers (Expires, Cache-Control, ETags and  Last-Modified)
  147.      *
  148.      * Expires and Cache-Control are sent as set.
  149.      *
  150.      * ETag is sent if set. Last-Modified is sent if set or if ETag isn't set,
  151.      * defaulting to the current time.
  152.      * Indeed, at least you of the two needs to be present for the response
  153.      * to be cacheable.
  154.      */
  155.     function sendHeaders(
  156.     {
  157.         // Signature Pxxo
  158.         header('X-Powered-By: Pxxo/5.x'false);
  159.  
  160.  
  161.         if ($this->isRedirect()) {
  162.             header('Location: ' $this->redirection[0]true);
  163.             header('HTTP/1.1 ' $this->redirection[1]);
  164.             return;
  165.         }
  166.  
  167.         // Cookies
  168.         foreach($this->cookies as $key => $value{
  169.             header('Set-Cookie: '.$valuefalse);
  170.         }
  171.  
  172.         // Expires corresponds to max-age
  173.         if ($this->ages['max-age'>= 0{
  174.             header('Expires: ' self::formatDate(time($this->ages['max-age'])1);
  175.         }
  176.         // Cache-Control
  177.         if (!is_array($this->_CacheDirectives)) {
  178.             $this->_CacheDirectives = array();
  179.         }
  180.         foreach($this->ages as $dir => $value{
  181.             if ($value >= 0{
  182.                 array_push($this->_CacheDirectives"$dir=$value");
  183.             }
  184.         }
  185.         if (count($this->_CacheDirectives0{
  186.             header('Cache-Control: ' .
  187.                 implode(', '$this->_CacheDirectives)true);
  188.         }
  189.         // At least one of ETags of Last-Modified will be sent for cacheability
  190.         if ($this->eTag{
  191.             header('ETag: ' $this->eTag);
  192.         }
  193.         if ($this->lastModified{
  194.             $lm $this->lastModified;
  195.         else if (!$this->eTag{
  196.             $lm time();
  197.         }
  198.         if (isset($lm)) {
  199.             header('Last-Modified: ' self::formatDate($lm));
  200.         }
  201.  
  202.         // Content-*
  203.         foreach($this->_ContentDirectives as $key => $value{
  204.             header('Content-'.ucfirst($key).': ' $valuetrue);
  205.         }
  206.     }
  207.  
  208.     /**
  209.      * Determines whether the client has a fresh representation of the resource.
  210.      *
  211.      * This function compares the If-Modified-Since and If-None-Match headers
  212.      * provided by the client to the values specified for this resource, and
  213.      * returns true if the resource has been modified or false if the client's
  214.      * version is current.
  215.      *
  216.      * @return true if the version the client has is fresh, false if this is not the case and a new version needs to be returned
  217.      */
  218.     function isFresh(
  219.     {
  220.         if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE']&&
  221.             !isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
  222.                 // No information provided by the client
  223.                 return false;
  224.             }
  225.  
  226.         if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']&& $this->eTag{
  227.             if (!$this->lastModified{
  228.                 $this->addCookie('T2'true);
  229.                 return false;
  230.             }
  231.             // Split the If-Modified-Since (Netscape < v6 gets this wrong)
  232.             $ifModifiedSince explode(';'$_SERVER['HTTP_IF_MODIFIED_SINCE']);
  233.             // Turn the client request If-Modified-Since into a timestamp
  234.             $ifModifiedSince strtotime($ifModifiedSince[0]);
  235.             // Compare timestamps (FIXME: make this test '!='?)
  236.             if ($this->lastModified > $ifModifiedSince{
  237.                 return false;
  238.             }
  239.         }
  240.  
  241.         if (isset($_SERVER['HTTP_IF_NONE_MATCH']&& $this->eTag{
  242.             if ($_SERVER['HTTP_IF_NONE_MATCH'== '*'{
  243.                 return true;
  244.             }
  245.             $etags preg_split('/,\s*/'$_SERVER['HTTP_IF_NONE_MATCH']);
  246.             foreach($etags as $e{
  247.                 if ($this->etagMatch($e)) {
  248.                     return true;
  249.                 }
  250.             }
  251.             return false;
  252.         }
  253.  
  254.         return true;
  255.     }
  256.  
  257.     /**
  258.      * Compares an etag with the resource's entity tag.
  259.      *
  260.      * @param $etag Entity tag to compare
  261.      * @return true if they match, false if they don't
  262.      */
  263.     private function etagMatch($etag
  264.     {
  265.         if (!$this->eTag{
  266.             return false;
  267.         }
  268.         if ((self::isEtagWeak($this->eTag|| self::isEtagWeak($etag))
  269.             &&
  270.                 ($_SERVER['REQUEST_METHOD'!= "GET" || isset($_SERVER['HTTP_RANGE'])))
  271.         {
  272.             // Weak validation only works for non-subrange GET requests
  273.             return false;
  274.         }
  275.         if (self::etagValidator($this->eTag== self::etagValidator($etag)) {
  276.             return true;
  277.         else {
  278.             return false;
  279.         }
  280.     }
  281.  
  282.     /**
  283.      * Determines if an entity tag is weak
  284.      *
  285.      * @param $etag Entity tag
  286.      * @return true if the entity tag is weak, false otherwise
  287.      */
  288.     static function isEtagWeak($etag
  289.     {
  290.         return (substr_compare($etag'W/'02== 0);
  291.     }
  292.  
  293.     /**
  294.      * Returns the validator value of an entity tag
  295.      *
  296.      * @param $etag Entity tag
  297.      * @return The validator value ($etag if the entity tag is strong, or what's after 'W/' otherwise)
  298.      */
  299.     static function etagValidator($etag)
  300.     {
  301.         if (self::isEtagWeak($etag)) {
  302.             return substr($etag2);
  303.         else {
  304.             return $etag;
  305.         }
  306.     }
  307.  
  308.     /**
  309.      * Sets Location header and response code. Forces replacement of any prior redirects.
  310.      *
  311.      * @param string $url 
  312.      * @param int $code 
  313.      */
  314.     public function setRedirect($url$code 302)
  315.     {
  316.         $this->redirection = array($url$code);
  317.     }
  318.  
  319.     /**
  320.      * Is there a redirection
  321.      *
  322.      * @return boolean 
  323.      */
  324.     public function isRedirect()
  325.     {
  326.         return (count($this->redirection=== 2);
  327.     }
  328.  
  329.  
  330.     /**
  331.      * Get the max-age or s-maxage cache control directive value
  332.      *
  333.      * @param $type "max-age" or "s-maxage"
  334.      * @return The value in seconds; if -1 is returned, no max-age directive will be sent
  335.      */
  336.     function getDuration($type
  337.     {
  338.         if ($type != "max-age" && $type != "s-maxage"{
  339.             return trigger_error('Invalid type in getDuration : `'.$type.' is unknown'E_USER_NOTICE);
  340.         }
  341.         return $this->ages[$type];
  342.     }
  343.  
  344.     /**
  345.      * Set max-age or s-maxage cache control directive value
  346.      *
  347.      * @param $type "max-age" or "s-maxage"
  348.      * @param $time Duration as a positive number of seconds, a string
  349.      *         for strtotime(), or a negative number to disable this directive
  350.      * @return Updated value
  351.      */
  352.     function setDuration($type$time
  353.     {
  354.         if ($type != "max-age" && $type != "s-maxage"{
  355.             return trigger_error('Invalid type in setDuration : `'.$type.' is unknown'E_USER_NOTICE);
  356.         }
  357.         if (is_numeric($time)) {
  358.             $time intval($time);
  359.             if ($time 0{
  360.                 $time = -1;
  361.             }
  362.         else {
  363.             if ($time == 'now'{
  364.                 $time 0;
  365.             else {
  366.                 $time strtotime($timetime();
  367.             }
  368.             if ($time 0{
  369.                 return trigger_error('Invalid time in setDuration : Bad interval specified to strtotime()'E_USER_NOTICE);
  370.             }
  371.         }
  372.         return $this->ages[$type$time;
  373.     }
  374.  
  375.     /**
  376.      * Set max-age and the Expires header value
  377.      *
  378.      * @param $time Duration as a positive number of seconds, a string
  379.      *         for strtotime(), or a negative number to disable this directive
  380.      * @return Updated value
  381.      */
  382.     function freshFor($time
  383.     {
  384.         return $this->setDuration('max-age'$time);
  385.     }
  386.  
  387.     /**
  388.      * Set/unset Cache-Control directive
  389.      *
  390.      * @param string $type Cache-Control directive (e.g. "public")
  391.      * @param boolean $set true or false to set or unset the parameter
  392.      */
  393.     function setCacheDirective($type$set
  394.     {
  395.         $type strtolower($type);
  396.         if (!in_array($type$this->CacheDirectives)) {
  397.             return trigger_error('Invalid type in setCacheDirective : `'.$type.' is unknown'E_USER_NOTICE);
  398.         }
  399.         if ($set == true{
  400.             if (!in_array($type$this->_CacheDirectives)) {
  401.                 array_push($this->_CacheDirectives$type);
  402.             }
  403.         else {
  404.             $this->_CacheDirectives = array_diff($this->_CacheDirectivesarray($type));
  405.         }
  406.     }
  407.  
  408.     /**
  409.      * Set/unset Content directive
  410.      *
  411.      * @param string $type Content directive (e.g. "type")
  412.      * @param mixed $set 
  413.      */
  414.     function setContentDirective($type$set
  415.     {
  416.         $type strtolower($type);
  417.         if (!in_array($type$this->ContentDirectives)) {
  418.             return trigger_error('Invalid type in setContentDirective : `'.$type.' is unknown'E_USER_NOTICE);
  419.         }
  420.         $this->_ContentDirectives[$type$set;
  421.     }
  422.  
  423.  
  424.  
  425.     /**
  426.      * Set value of the ETag header
  427.      * The string will be quoted automatically.
  428.      *
  429.      * @param string $value Value (string)
  430.      */
  431.     function setEtag($value
  432.     {
  433.         $this->eTag = '"' $value '"';
  434.     }
  435.  
  436.     /**
  437.      * Set value of the ETag header to a weak value
  438.      * The string will be quoted automatically.
  439.      *
  440.      * @param string $value Value (string)
  441.      */
  442.     function setWeakEtag($value
  443.     {
  444.         $this->setEtag($value);
  445.         $this->eTag = "W/" $this->eTag;
  446.     }
  447.  
  448.     /**
  449.      * Set value of the Last-Modified header
  450.      *
  451.      * @param string $value Unix timestamp
  452.      */
  453.     function setLastModified($value
  454.     {
  455.         $this->lastModified = $value;
  456.     }
  457.  
  458.     /**
  459.      * Set value of the Last-Modified header using the last modification date of a file
  460.      *
  461.      * @param string $file File whose modification time will be used
  462.      */
  463.     function setLastModifiedFromFile($file
  464.     {
  465.         $finfo stat($file);
  466.         if (!$finfo{
  467.             return;
  468.         }
  469.         $this->setLastModified($finfo['mtime']);
  470.     }
  471.  
  472.     /**
  473.      * Format a date in RFC 1123 date format
  474.      *
  475.      * @param string $time Time since epoch in seconds
  476.      * @return string Formatted date (e.g. "Sun, 27 Aug 2006 08:32:28 GMT")
  477.      */
  478.     static function formatDate($time
  479.     {
  480.         return gmdate("D, d M Y H:i:s"$time' GMT';
  481.     }
  482.  
  483.  
  484.     /**
  485.      * setCookie (RFC 2109 compatible)
  486.      *
  487.      * @param string Name of the cookie
  488.      * @param string Value of the cookie
  489.      * @param int Lifetime of the cookie
  490.      * @param string Path where the cookie can be used
  491.      * @param string Domain which can read the cookie
  492.      * @param bool Secure mode?
  493.      * @param bool Only allow HTTP usage?
  494.      * @return bool True or false whether the method has successfully run
  495.      */
  496.     function addCookie($name$value=''$maxage=0$path=''$domain=''$secure=false$HTTPOnly=false)
  497.     {
  498.         $ob ini_get('output_buffering');
  499.  
  500.         // Abort the method if headers have already been sent, except when output buffering has been enabled
  501.         if headers_sent(&& (bool) $ob === false || strtolower($ob== 'off' )
  502.             return false;
  503.  
  504.         if !empty($domain) )
  505.         {
  506.             // Fix the domain to accept domains with and without 'www.'.
  507.             if strtolowersubstr($domain04) ) == 'www.' $domain substr($domain4);
  508.             // Add the dot prefix to ensure compatibility with subdomains
  509.             if substr($domain01!= '.' $domain '.'.$domain;
  510.  
  511.             // Remove port information.
  512.             $port strpos($domain':');
  513.  
  514.             if $port !== false $domain substr($domain0$port);
  515.         }
  516.  
  517.         $this->cookies[$namerawurlencode($name).'='.rawurlencode($value)
  518.         .(empty($domain'' '; Domain='.$domain)
  519.         .(empty($maxage'' '; Max-Age='.$maxage)
  520.         .(empty($path'' '; Path='.$path)
  521.         .(!$secure '' '; Secure')
  522.         .(!$HTTPOnly '' '; HttpOnly');
  523.  
  524.         return true;
  525.     }
  526.  
  527. }

Documentation generated on Thu, 13 Mar 2008 22:03:22 +0100 by phpDocumentor 1.4.1