Auth_HTTP
[ class tree: Auth_HTTP ] [ index: Auth_HTTP ] [ all elements ]

Source for file Auth_HTTP.php

Documentation is available at Auth_HTTP.php

  1. <?php
  2. //
  3. // +----------------------------------------------------------------------+
  4. // | PHP Version 4                                                        |
  5. // +----------------------------------------------------------------------+
  6. // | Copyright (c) 1997-2004 The PHP Group                                |
  7. // +----------------------------------------------------------------------+
  8. // | This source file is subject to version 2.02 of the PHP license,      |
  9. // | that is bundled with this package in the file LICENSE, and is        |
  10. // | available at through the world-wide-web at                           |
  11. // | http://www.php.net/license/2_02.txt.                                 |
  12. // | If you did not receive a copy of the PHP license and are unable to   |
  13. // | obtain it through the world-wide-web, please send a note to          |
  14. // | license@php.net so we can mail you a copy immediately.               |
  15. // +----------------------------------------------------------------------+
  16. // | Authors: Martin Jansen <mj@php.net>                                  |
  17. // |          Rui Hirokawa <hirokawa@php.net>                             |
  18. // |          David Costa  <gurugeek@php.net>                             |
  19. // +----------------------------------------------------------------------+
  20. //
  21. //  $Id: Auth_HTTP.php,v 1.16 2004/04/16 14:14:16 gurugeek Exp $ 
  22. //
  23.  
  24. require_once "Auth/Auth.php";
  25.  
  26. define('AUTH_HTTP_NONCE_TIME_LEN'16);
  27. define('AUTH_HTTP_NONCE_HASH_LEN'32);
  28.  
  29. // {{{ class Auth_HTTP
  30.  
  31. /**
  32.  * PEAR::Auth_HTTP
  33.  *
  34.  * The PEAR::Auth_HTTP class provides methods for creating an
  35.  * HTTP authentication system based on RFC-2617 using PHP.
  36.  *
  37.  * Instead of generating an HTML driven form like PEAR::Auth
  38.  * does, this class sends header commands to the clients which
  39.  * cause them to present a login box like they are e.g. used
  40.  * in Apache's .htaccess mechanism.
  41.  *
  42.  * This class requires the PEAR::Auth package.
  43.  *
  44.  * @notes The HTTP Diegest Authentication part is based on
  45.  *   authentication class written by Tom Pike <tom.pike@xiven.com>
  46.  *
  47.  * @author  Martin Jansen <mj@php.net>
  48.  * @author  Rui Hirokawa <hirokawa@php.net>
  49.  * @author  David Costa <gurugeek@php.net>
  50.  * @package Auth_HTTP
  51.  * @extends Auth
  52.  * @version $Revision: 1.16 $
  53.  */
  54. class Auth_HTTP extends Auth
  55. {
  56.    
  57.     // {{{ properties
  58.  
  59.     /**
  60.      * Authorization method: 'basic' or 'digest'
  61.      *
  62.      * @access public
  63.      * @var    string 
  64.      */
  65.     var $authType = 'basic';
  66.  
  67.     /**
  68.      * Name of the realm for Basic Authentication
  69.      *
  70.      * @access public
  71.      * @var    string 
  72.      * @see    drawLogin()
  73.      */
  74.     var $realm = "protected area";
  75.  
  76.     /**
  77.      * Text to send if user hits cancel button
  78.      *
  79.      * @access public
  80.      * @var    string 
  81.      * @see    drawLogin()
  82.      */
  83.     var $CancelText = "Error 401 - Access denied";
  84.  
  85.     /**
  86.      * option array
  87.      *
  88.      * @access public
  89.      * @var    array 
  90.      */
  91.     var $options = array();
  92.  
  93.     /**
  94.      * flag to indicate the nonce was stale.
  95.      *
  96.      * @access public
  97.      * @var    bool 
  98.      */
  99.     var $stale = false;
  100.  
  101.     /**
  102.      * opaque string for digest authentication
  103.      *
  104.      * @access public
  105.      * @var    string 
  106.      */
  107.     var $opaque = 'dummy';
  108.  
  109.     /**
  110.      * digest URI
  111.      *
  112.      * @access public
  113.      * @var    string 
  114.      */
  115.     var $uri = '';
  116.  
  117.     /**
  118.      * authorization info returned by the client
  119.      *
  120.      * @access public
  121.      * @var    array 
  122.      */
  123.     var $auth = array();
  124.  
  125.     /**
  126.      * next nonce value
  127.      *
  128.      * @access public
  129.      * @var    string 
  130.      */
  131.     var $nextNonce = '';
  132.  
  133.     /**
  134.      * nonce value
  135.      *
  136.      * @access public
  137.      * @var    string 
  138.      */
  139.     var $nonce = '';
  140.  
  141.     // }}}
  142.     // {{{ Constructor
  143.  
  144.     /**
  145.      * Constructor
  146.      *
  147.      * @param string    Type of the storage driver
  148.      * @param mixed     Additional options for the storage driver
  149.      *                   (example: if you are using DB as the storage
  150.      *                    driver, you have to pass the dsn string here)
  151.      *
  152.      * @return void 
  153.      */
  154.     function Auth_HTTP($storageDriver$options ''
  155.     {
  156.         /* set default values for options */
  157.         $this->options = array('cryptType' => 'md5',
  158.                    'algorithm' => 'MD5',
  159.                                'qop' => 'auth-int,auth',
  160.                                'opaquekey' => 'moo',
  161.                                'noncekey' => 'moo',
  162.                                'digestRealm' => 'protected area',
  163.                                'forceDigestOnly' => false,
  164.                                'nonceLife' => 300,
  165.                                'sessionSharing' => true,
  166.                                );
  167.         
  168.         if (!empty($options['authType'])) {
  169.             $this->authType = strtolower($options['authType']);
  170.         }
  171.         
  172.         foreach($options as $key => $value{
  173.             if (array_key_exists$key$this->options)) {
  174.                 $this->options[$key$value;
  175.             }
  176.         }
  177.         
  178.         if (!empty($this->options['opaquekey'])) {
  179.             $this->opaque = md5($this->options['opaquekey']);
  180.         }
  181.         
  182.         $this->Auth($storageDriver$options);
  183.     }
  184.     
  185.     // }}}
  186.     // {{{ assignData()
  187.  
  188.     /**
  189.      * Assign values from $PHP_AUTH_USER and $PHP_AUTH_PW or 'Authorization' header
  190.      * to internal variables and sets the session id based
  191.      * on them
  192.      *
  193.      * @return void 
  194.      */
  195.     function assignData()
  196.     {
  197.         $server &$this->_importGlobalVariable('server');
  198.  
  199.         if ($this->authType == 'basic'{
  200.             if (!empty($server['PHP_AUTH_USER'])) {
  201.                 $this->username $server['PHP_AUTH_USER'];
  202.             }
  203.             
  204.             if (!empty($server['PHP_AUTH_PW'])) {
  205.                 $this->password $server['PHP_AUTH_PW'];
  206.             }
  207.             
  208.             /**
  209.              * Try to get authentication information from IIS
  210.              */
  211.             if  (empty($this->username&& empty($this->password)) {
  212.                 if (!empty($server['HTTP_AUTHORIZATION'])) {
  213.                     list($this->username$this->password
  214.                         explode(':'base64_decode(substr($server['HTTP_AUTHORIZATION']6)));
  215.                 }
  216.             }
  217.         elseif ($this->authType == 'digest'{
  218.          $this->username '';
  219.          $this->password '';
  220.  
  221.             $headers = getallheaders();
  222.             if(isset($headers['Authorization']&& !empty($headers['Authorization'])) {
  223.                 $authtemp explode(','substr($headers['Authorization'],
  224.                                                 strpos($headers['Authorization'],' ')+1));
  225.                 $auth = array();
  226.                 foreach($authtemp as $key => $value{
  227.                     $value trim($value);
  228.                     if(strpos($value,'='!== false{
  229.                         $lhs substr($value,0,strpos($value,'='));
  230.                         $rhs substr($value,strpos($value,'=')+1);
  231.                         if(substr($rhs,0,1== '"' && substr($rhs,-1,1== '"'{
  232.                             $rhs substr($rhs,1,-1);
  233.                         }
  234.                         $auth[$lhs$rhs;
  235.                     }
  236.                 }
  237.             }
  238.             if (!isset($auth['uri']|| !isset($auth['realm'])) {
  239.                 return;
  240.             }
  241.             
  242.             if ($server['PHP_SELF'== $auth['uri']{
  243.                 $this->uri = $auth['uri'];
  244.                 if (substr($headers['Authorization'],0,7== 'Digest '{
  245.                     
  246.                     $this->authType = 'digest';
  247.  
  248.                     if (!isset($auth['nonce']|| !isset($auth['username']|| 
  249.                   !isset($auth['response']|| !isset($auth['qop']|| 
  250.                   !isset($auth['nc']|| !isset($auth['cnonce'])){
  251.                         return;
  252.                     }
  253.  
  254.                if ($auth['qop'!= 'auth' && $auth['qop'!= 'auth-int'{
  255.                         return;
  256.                }
  257.                     
  258.                     $this->stale = $this->_judgeStale($auth['nonce']);
  259.  
  260.                if ($this->nextNonce == false{
  261.                   return;
  262.                }
  263.  
  264.                     $this->username $auth['username'];
  265.                     $this->password $auth['response'];
  266.                     $this->auth['nonce'$auth['nonce'];
  267.                     
  268.                $this->auth['qop'$auth['qop'];
  269.                $this->auth['nc'$auth['nc'];
  270.                $this->auth['cnonce'$auth['cnonce'];
  271.  
  272.                     if (isset($auth['opaque'])) {
  273.                         $this->auth['opaque'$auth['opaque'];
  274.                     }
  275.                     
  276.                 elseif (substr($headers['Authorization'],0,6== 'Basic '{
  277.                     if ($this->options['forceDigestOnly']{
  278.                         return// Basic authentication is not allowed.
  279.                     }
  280.                     
  281.                     $this->authType = 'basic';
  282.                     list($username$password
  283.                         explode(':',base64_decode(substr($headers['Authorization'],6)));
  284.                     $this->username $username;
  285.                     $this->password $password;
  286.                 }
  287.             }
  288.         else {
  289.             return PEAR::raiseError('authType is invalid.');
  290.         }
  291.  
  292.         if ($this->options['sessionSharing'&& 
  293.             isset($this->username&& isset($this->password)) {
  294.             session_id(md5('Auth_HTTP' $this->username $this->password));
  295.         }
  296.         
  297.         /**
  298.          * set sessionName for AUTH, so that the sessionName is different 
  299.          * for distinct realms 
  300.          */
  301.          $this->_sessionName "_authhttp".md5($this->realm);
  302.     }
  303.  
  304.     // }}}
  305.     // {{{ login()
  306.  
  307.     /**
  308.      * Login function
  309.      *
  310.      * @access private
  311.      * @return void 
  312.      */
  313.     function login(
  314.     {
  315.         $login_ok = false;
  316.  
  317.         /**
  318.          * When the user has already entered a username,
  319.          * we have to validate it.
  320.          */
  321.         if (!empty($this->username&& !empty($this->password)) {
  322.             if ($this->authType == 'basic' && !$this->options['forceDigestOnly']{
  323.                 if (true === $this->storage->fetchData($this->username$this->password)) {
  324.                     $login_ok = true;
  325.                 }
  326.             else /* digest authentication */
  327.  
  328.             if (!$this->getAuth(|| $this->getAuthData('a1'== null{
  329.                /* 
  330.                 * note:
  331.                 *  - only PEAR::DB is supported as container.
  332.                 *  - password should be stored in container as plain-text 
  333.                 *    (if $options['cryptType'] == 'none') or 
  334.                 *     A1 hashed form (md5('username:realm:password')) 
  335.                 *    (if $options['cryptType'] == 'md5')
  336.                 */
  337.                $dbs $this->storage;
  338.                if (!DB::isConnection($dbs->db)) {
  339.                   $dbs->_connect($dbs->options['dsn']);
  340.                }
  341.                
  342.                $query 'SELECT '.$dbs->options['passwordcol']." FROM ".$dbs->options['table'].
  343.                   ' WHERE '.$dbs->options['usernamecol']." = '".
  344.                   $dbs->db->quoteString($this->username)."' ";
  345.                
  346.                $pwd $dbs->db->getOne($query)// password stored in container.
  347.                
  348.                if (DB::isError($pwd)) {
  349.                   return PEAR::raiseError($pwd->getMessage()$pwd->getCode());
  350.                }
  351.                
  352.                if ($this->options['cryptType'== 'none'{
  353.                   $a1 md5($this->username.':'.$this->options['digestRealm'].':'.$pwd);
  354.                else {
  355.                   $a1 $pwd;
  356.                }
  357.  
  358.                $this->setAuthData('a1'$a1true);
  359.             else {
  360.                $a1 $this->getAuthData('a1');
  361.             }
  362.  
  363.                 $login_ok $this->validateDigest($this->password$a1);
  364.             if ($this->nextNonce == false{
  365.                $login_ok = false;
  366.             }
  367.             }
  368.  
  369.             if (!$login_ok && is_callable($this->loginFailedCallback)) {
  370.                 call_user_func($this->loginFailedCallback,$this->username$this);
  371.             }
  372.         }
  373.         
  374.         if (!empty($this->username&& $login_ok{
  375.             $this->setAuth($this->username);
  376.             if (is_callable($this->loginCallback)) {
  377.                 call_user_func($this->loginCallback,$this->username$this);
  378.             }
  379.         }
  380.         
  381.         /**
  382.          * If the login failed or the user entered no username,
  383.          * output the login screen again.
  384.          */
  385.         if (!empty($this->username&& !$login_ok{
  386.             $this->status = AUTH_WRONG_LOGIN;
  387.         }
  388.         
  389.         if ((empty($this->username|| !$login_ok&& $this->showLogin{
  390.             $this->drawLogin($this->storage->activeUser);
  391.             return;
  392.         }
  393.  
  394.       if (!empty($this->username&& $login_ok && $this->authType == 'digest'
  395.          && $this->auth['qop'== 'auth'
  396.          $this->authenticationInfo();
  397.       }
  398.     }
  399.     
  400.     // }}}
  401.     // {{{ drawLogin()
  402.  
  403.     /**
  404.      * Launch the login box
  405.      *
  406.      * @param  string $username  Username
  407.      * @return void 
  408.      */
  409.     function drawLogin($username "")
  410.     {
  411.         /**
  412.          * Send the header commands
  413.          */
  414.         if ($this->authType == 'basic'{
  415.             header("WWW-Authenticate: Basic realm=\"".$this->realm."\"");
  416.             header('HTTP/1.0 401 Unauthorized');            
  417.         else if ($this->authType == 'digest'{
  418.             $this->nonce = $this->_getNonce();
  419.  
  420.             $wwwauth 'WWW-Authenticate: Digest ';
  421.             $wwwauth .= 'qop="'.$this->options['qop'].'", ';
  422.             $wwwauth .= 'algorithm='.$this->options['algorithm'].', ';
  423.             $wwwauth .= 'realm="'.$this->options['digestRealm'].'", ';
  424.             $wwwauth .= 'nonce="'.$this->nonce.'", ';
  425.             if ($this->stale{
  426.                 $wwwauth .= 'stale=true, ';
  427.             }
  428.             if (!empty($this->opaque)) {
  429.                 $wwwauth .= 'opaque="'.$this->opaque.'"' ;
  430.             }
  431.             $wwwauth .= "\r\n";
  432.             if (!$this->options['forceDigestOnly']{
  433.                 $wwwauth .= 'WWW-Authenticate: Basic realm="'.$this->realm.'"';
  434.             }
  435.             header($wwwauth);
  436.             header('HTTP/1.0 401 Unauthorized');            
  437.         }
  438.  
  439.         /**
  440.          * This code is only executed if the user hits the cancel
  441.          * button or if he enters wrong data 3 times.
  442.          */
  443.         if ($this->stale{
  444.             echo 'Stale nonce value, please re-authenticate.';
  445.         else {
  446.             print_r($this->options)// debug
  447.             echo $this->CancelText;
  448.         }
  449.         exit;
  450.     }
  451.  
  452.     // }}}
  453.     // {{{ setRealm()
  454.  
  455.     /**
  456.      * Set name of the current realm
  457.      *
  458.      * @access public
  459.      * @param  string $realm  Name of the realm
  460.      * @param  string $digestRealm  Name of the realm for digest authentication
  461.      * @return void 
  462.      */
  463.     function setRealm($realm$digestRealm '')
  464.     {
  465.         $this->realm = $realm;
  466.         if (!empty($digestRealm)) {
  467.             $this->options['digestRealm'$digestRealm;
  468.         }
  469.     }
  470.  
  471.     // }}}
  472.     // {{{ setCancelText()
  473.  
  474.     /**
  475.      * Set the text to send if user hits the cancel button
  476.      *
  477.      * @access public
  478.      * @param  string $text  Text to send
  479.      * @return void 
  480.      */
  481.     function setCancelText($text)
  482.     {
  483.         $this->CancelText = $text;
  484.     }
  485.  
  486.     // }}}
  487.     // {{{ validateDigest()
  488.     
  489.     /**
  490.      * judge if the client response is valid.
  491.      *
  492.      * @access public
  493.      * @param  string $response  client response
  494.      * @param  string $a1 password or hashed password stored in container
  495.      * @return bool true if success, false otherwise
  496.      */
  497.     function validateDigest($response$a1)    
  498.     {
  499.         $server &$this->_importGlobalVariable('server');
  500.  
  501.         $a2unhashed $server['REQUEST_METHOD'].":".$server['PHP_SELF'];
  502.         if($this->auth['qop'== 'auth-int'{
  503.             if(isset($GLOBALS["HTTP_RAW_POST_DATA"])) {
  504.                 // In PHP < 4.3 get raw POST data from this variable
  505.                 $body $GLOBALS["HTTP_RAW_POST_DATA"];
  506.             else if($lines @file('php://input')) {
  507.                 // In PHP >= 4.3 get raw POST data from this file
  508.                 $body implode("\n"$lines);
  509.             else {
  510.                 $post &$this->_importGlobalVariable('post');
  511.                 $body '';
  512.                 foreach($post as $key => $value{
  513.                     if($body != ''$body .= '&';
  514.                     $body .= rawurlencode($key'=' rawurlencode($value);
  515.                 }
  516.             }
  517.  
  518.             $a2unhashed .= ':'.md5($body);
  519.         }
  520.         
  521.         $a2 md5($a2unhashed);
  522.         $combined $a1.':'.
  523.             $this->auth['nonce'].':'.
  524.             $this->auth['nc'].':'.
  525.             $this->auth['cnonce'].':'.
  526.             $this->auth['qop'].':'.
  527.             $a2;
  528.         $expectedResponse md5($combined);
  529.         
  530.         if(!isset($this->auth['opaque']|| $this->auth['opaque'== $this->opaque{
  531.             if($response == $expectedResponse// password is valid
  532.                 if(!$this->stale{
  533.                     return true;
  534.                 else {
  535.                     $this->drawLogin();
  536.                 }
  537.             }
  538.         }
  539.         
  540.         return false;
  541.     }
  542.     
  543.     // }}}
  544.     // {{{ _judgeStale()
  545.     
  546.     /**
  547.      * judge if nonce from client is stale.
  548.      *
  549.      * @access private
  550.      * @param  string $nonce  nonce value from client
  551.      * @return bool stale
  552.      */
  553.     function _judgeStale($nonce
  554.     {
  555.         $stale = false;
  556.         
  557.         if(!$this->_decodeNonce($nonce$time$hash_cli)) {
  558.          $this->nextNonce = false;
  559.          $stale = true;
  560.             return $stale;
  561.         }
  562.  
  563.         if ($time time($this->options['nonceLife']{
  564.          $this->nextNonce = $this->_getNonce();
  565.             $stale = true;
  566.         else {
  567.          $this->nextNonce = $nonce;
  568.       }
  569.  
  570.         return $stale;
  571.     }
  572.     
  573.     // }}}
  574.     // {{{ _nonceDecode()
  575.     
  576.     /**
  577.      * decode nonce string
  578.      *
  579.      * @access private
  580.      * @param  string $nonce nonce value from client
  581.      * @param  string $time decoded time
  582.      * @param  string $hash decoded hash
  583.      * @return bool false if nonce is invalid
  584.      */
  585.     function _decodeNonce($nonce&$time&$hash
  586.     {
  587.         $server &$this->_importGlobalVariable('server');
  588.  
  589.         if (strlen($nonce!= AUTH_HTTP_NONCE_TIME_LEN + AUTH_HTTP_NONCE_HASH_LEN{
  590.             return false;
  591.         }
  592.  
  593.         $time =  base64_decode(substr($nonce0AUTH_HTTP_NONCE_TIME_LEN));
  594.         $hash_cli substr($nonceAUTH_HTTP_NONCE_TIME_LENAUTH_HTTP_NONCE_HASH_LEN);
  595.  
  596.         $hash md5($time $server['HTTP_USER_AGENT'$this->options['noncekey']);
  597.  
  598.         if ($hash_cli != $hash{
  599.             return false;
  600.         }
  601.         
  602.         return true;
  603.     }
  604.  
  605.     // }}}
  606.     // {{{ _getNonce()
  607.     
  608.     /**
  609.      * return nonce to detect timeout
  610.      *
  611.      * @access private
  612.      * @return string nonce value
  613.      */
  614.     function _getNonce(
  615.     {
  616.         $server &$this->_importGlobalVariable('server');
  617.  
  618.         $time time();
  619.         $hash md5($time $server['HTTP_USER_AGENT'$this->options['noncekey']);
  620.  
  621.         return base64_encode($time$hash;  
  622.     }
  623.  
  624.     // }}}
  625.     // {{{ authenticationInfo()
  626.     
  627.     /**
  628.      * output HTTP Authentication-Info header
  629.      *
  630.      * @notes md5 hash of contents is required if 'qop' is 'auth-int'
  631.      *
  632.      * @access public
  633.      * @param string MD5 hash of content
  634.      */
  635.     function authenticationInfo($contentMD5 ''{
  636.         
  637.         if($this->getAuth(&& ($this->getAuthData('a1'!= null)) {
  638.             $server &$this->_importGlobalVariable('server');
  639.             $a1 $this->getAuthData('a1');
  640.  
  641.             // Work out authorisation response
  642.             $a2unhashed ":".$server['PHP_SELF'];
  643.             if($this->auth['qop'== 'auth-int'{
  644.                 $a2unhashed .= ':'.$contentMD5;
  645.             }
  646.             $a2 md5($a2unhashed);
  647.             $combined $a1.':'.
  648.                         $this->nonce.':'.
  649.                         $this->auth['nc'].':'.
  650.                         $this->auth['cnonce'].':'.
  651.                         $this->auth['qop'].':'.
  652.                         $a2;
  653.             
  654.             // Send authentication info
  655.             $wwwauth 'Authentication-Info: ';
  656.             if($this->nonce != $this->nextNonce{
  657.                 $wwwauth .= 'nextnonce="'.$this->nextNonce.'", ';
  658.             }
  659.             $wwwauth .= 'qop='.$this->auth['qop'].', ';
  660.             $wwwauth .= 'rspauth="'.md5($combined).'", ';
  661.             $wwwauth .= 'cnonce="'.$this->auth['cnonce'].'", ';
  662.             $wwwauth .= 'nc='.$this->auth['nc'].'';
  663.             header($wwwauth);
  664.         }
  665.     }
  666.     // }}}
  667.     // {{{ setOption()
  668.     /**
  669.      * set authentication option
  670.      *
  671.      * @access public
  672.      * @param mixed $name key of option
  673.      * @param mixed $value value of option
  674.      * @return void 
  675.      */
  676.     function setOption($name$value = null
  677.     {
  678.         if (is_array($name)) {
  679.             foreach($name as $key => $value{
  680.                 if (array_key_exists$key$this->options)) {
  681.                     $this->options[$key$value;
  682.                 }
  683.             }
  684.         else {
  685.             if (array_key_exists$name$this->options)) {
  686.                     $this->options[$name$value;
  687.             }
  688.         }
  689.     }
  690.  
  691.     // }}}
  692.     // {{{ getOption()
  693.     /**
  694.      * get authentication option
  695.      *
  696.      * @access public
  697.      * @param string $name key of option
  698.      * @return mixed option value
  699.      */
  700.     function getOption($name
  701.     {
  702.         if (array_key_exists$name$this->options)) {
  703.             return $this->options[$name];
  704.         }
  705.         if ($name == 'CancelText'{
  706.             return $this->CancelText;
  707.         }
  708.         if ($name == 'Realm'{
  709.             return $this->realm;
  710.         }
  711.         return false;
  712.     }
  713.  
  714.     // }}}
  715. }
  716.  
  717. // }}}
  718.  
  719. /*
  720.  * Local variables:
  721.  * tab-width: 4
  722.  * c-basic-offset: 4
  723.  * End:
  724.  */
  725. ?>

Documentation generated on Mon, 11 Mar 2019 10:15:01 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.