Initial draft done: feedback please

[email protected] (Mike) Wed, 14 Mar 2001 15:23:06 GMT
Newsgroups php.pear
Message-ID <1105_984583386@matthew>
Comments please, as this is my first attempt at writing for PEAR.

It's an HTTP client library.

It's not finished - I'm looking for interim feedback - not just criticism of my code, but also feedback as to where this should go.

Is it desirable to create something as big as perl's libwww? Would, for instance, it be good to have separate HTTP message classes and so on?

My instincts are towards a relatively simple class that simply sends an HTTP message to a server and reads and parses (*) the response.

I had a couple of problems:

1. I had to change     
                if ($len >=2 && substr($line, $len-2, 2) == "\r\n")
in
function readLine() {
        if (is_resource($this->fp)) {
            $line = '';
            $timeout = time() + $this->timeout;
            while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) {
                $line .= fgets($this->fp, $this->lineLength);
                $len = strlen($line);
                if ($len >2 && substr($line, $len-2, 2) == "\r\n")
                    return substr($line, 0, $len-2);
            }
            return $line;
        }
        return new PEAR_ERROR("not connected");
    }

to                 if ($len >2 && substr($line, $len-2, 2) == "\r\n")

in the Net_Socket class; otherwise

        while ($line = $this->socket->readLine()) { 

evaluates to false at the end of the HTTP header.

2. Also, the Net_Socket is not robust. I have had ->write attempts take up to half-an-hour to complete (connection to the socket with a timeout works fine).

--

Usage is thus:

$net=new Net_HTTP();
print $net->query("POST","http://php.net/manual-lookup.php",array(pattern=>"test"));
or
print $net->query("GET","http://www.google.co.uk");

It supports proxies; e.g.,

$net->setconnectionprefs("go.becker.edu",8080);
print $net->query("GET","http://www.google.co.uk");

as well as cookies.

* The parsing is one thing I haven't really done so far; the parseHeader() method is far from a complete HTTP parser. Also unimplemented is proper responses to HTTP codes; following redirects for instance.

<?php
require_once 'PEAR.php';
header("content-type: text/plain");
/**
 * HTTP client class; uses Net_Socket
 * getting to file
 * and posting a file 
 * content negotiation?
 * basic authentication 
 * support for compression and decompression
 * more support for parsing the header response from the server - 404s etc.
 *
 */
class Net_HTTP extends PEAR  {

    /**
     * The HTTP User Agent to identify as
     * @var string
     */
    var $ua="Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)";

    /**
     * The proxy server to use
     * @var string
     */
    var $proxy=null;

    /**
     * The proxy port to use
     * @var int
     */
    var $int=80;

    /**
     * The timeout of socket connect attempts in seconds
     * @var int
     */
    var $timeout=10;

    /**
     * The timeout of socket read and writes in seconds
     * @var int
     */
    var $socketseconds=null;

    /**
     * The HTTP protocol to use
     * @var string
     */
    var $protocol="1.0";

    /**
     * Whether to send referer headers
     * @var bool
     */
    var $referer=false;

    /**
     * The value of the HTTP Accept: header
     * @var string
     */
    var $accept="*/*";

    /**
     * The value of the HTTP Accept-Language: header
     * @var string
     */
    var $accept_language=null;

    /**
     * The timeout of socket read and writes in miliseconds
     * @var int
     */
    var $socketmiliseconds=null;

    /**
     * Constructor
     *
     * Instantiates a new Net_HTTP object
     */
    function Net_HTTP() {
    }

    /**
     * Modify HTTP header defaults for the Net_HTTP object
     *
     * @param string User agent name
     * @param string Value of Accept header
     * @param string Value of Accept-language header
     * @param array key => value pairs for any extra HTTP headers
     *
     * @return bool Always returns true. If it doesn't, you've got trouble.
     * @access public
     */
    function setheaders($ua=null,$accept=null,$accept_language=null,$headers=null) {
        if (isset($headers)) {
            $this->headers='';
            foreach ($headers as $key=>$value) {
                $this->headers .= $key.': '.$value;
            }
        }
        if (isset($accept)) $this->accept=$accept;
        if (isset($accept_language)) $this->accept_language=$accept_language;
        if (isset($ua)) $this->ua=$ua;
    }

    /**
     * Modify connection preferences for the Net_HTTP object
     *
     * @param string proxy to connect via
     * @param int proxy port to use
     * @param int timeout on connect attempts
     * @param int timeout on socket read/write attempts in seconds
     * @param int timeout on socket read/write attempts in miliseconds
     *
     * @return bool Always returns true. If it doesn't, you've got trouble.
     * @access public
     */
    function setconnectionprefs($proxy=null,$proxyport=null,$timeout=null,$socketseconds=null,$socketmiliseconds=null) {
        if (isset($proxy)) $this->proxy=$proxy;
        if (isset($proxyport)) $this->proxyport=$proxyport;
        if (isset($timeout)) $this->timeout=$timeout;
        if (isset($socketseconds)) $this->socketseconds=$socketseconds;
        if (isset($socketmiliseconds)) $this->socketmiliseconds=$socketmiliseconds;
        return true;
    }

    /**
     * Send one of the supported HTTP queries. Currently GET, POST and HEAD
     *
     * @param string the HTTP type. GET, HEAD or POST
     * @param string URI to post to.
     * @param array Optional array of variables
     * @param array Optional array of cookies
     *
     * @return mixed Returns true on success, or a PEAR_error indicating
     * the problem on failure
     * @access public
     */
    function query($type,$url,$variables='',$cookies='') {
        $type=strtoupper($type); // HTTP is case-insensitive
        switch ($type) { 
            case GET:
            case POST:
            case HEAD:
                break;
            default:
                return $this->raiseError(20,array($type));
        }
        if ($variables) {
            foreach ($variables as $key=>$value) {
                if ($encodedvars) {
                    $encodedvars.="&".$key."=".urlencode($value);
                }
                else {
                    $encodedvars=$key."=".urlencode($value);
                }
            }
        }
        $parsed_url = $this->checkuri($url);
        if (!is_array($parsed_url)) {
            $this->raiseError($parsed_url);
        }
        $referer=$url;
        $query="$type ";
        if ($this->proxy) {
            $query.="http://".$parsed_url["host"];
        }
        $query.=$parsed_url["path"];
        if ($type=="GET" && $encodedvars) {
            $query.="?".$encodedvars;
        }
        $query.=" HTTP/$this->protocol\r\n";
        $query.=$this->headers;
        if (isset($this->accept)) $query.="Accept: $this->accept\r\n";
        if (isset($this->referer)) {
            if (!$referer) $referer=$parsed_url;
            $query.="Referer: $referer\r\n";
        }
        if (isset($this->accept_language)) $query.="Accept-Language: $this->accept_language\r\n";
        $query.="User-Agent: $this->ua\r\n";
        if ($type=="POST") {
//FIXME: $this is not always right. Will need conditional when file upload support is added
           $query.="Content-type: application/x-www-form-urlencoded\r\n";
        }
        $query.="Host: ".$parsed_url["host"]."\r\n";
        if ($type=="POST") {
           $query.="Content-length: ".strlen($encodedvars)."\r\n"; 
        }
        if ($cookies) {
            foreach ($cookies as $cookie) {
                $query.="Cookie: ".$cookie["key"]."=".urlencode($cookie["value"])."\r\n";
            }
        }
        $query.="Connection: close\r\n\r\n";
        if ($type=="POST") {
           $query.=$encodedvars;
        }
        if ($this->proxy) {
            return $this->request($this->proxy,$query,$this->proxyport);
        }
        else {
            return $this->request($parsed_url["host"],$query);
        }
    }

    /**
     * Send an HTTP request
     *
     * @param string the host to connect to
     * @param string the raw HTTP request
     * @param int the port to connect to
     *
     * @return mixed Returns true on success, or a PEAR_error indicating
     * the problem on failure
     * @access public
     */
    function request($host,$query,$port=80) {
        if (PEAR::isError($this->connect($host,$port))) {
            return $this->raiseError(601,array($host,$port));
        }
        $error = $this->socket->write($query);
        if (PEAR::isError($error)) {
           return $this->raiseError(602,array($host,$port));
        }
        while ($line = $this->socket->readLine()) { // Hmm
            $headers=array();
            if ($flag) $body.=$line;
            else {
                $header=$this->parseHeader($line);
                array_push($headers,$header);
                if ($header["key"]=="location") {
// writeme
                }
                switch ($header["code"]) {
                    case 200:
                        break;
                    case 404:
                        break;
                    case 302:
                        break;
                    case null:
                        break;
                    default:
                        return $this->raiseError(610,$header["code"]);
                }
                if (!$header) {
                    $flag=true;
                    $body.=$line;
                }
            }
        }
        if (PEAR::isError($this->response)) {
            return $this->raiseError(603,array($host,$port));
        }
        return $body;
    }

    /**
     * Parse an HTTP Header
     *
     * @param string the header
     *
     * @return mixed Returns an array if it's a valid HTTP header or else false
     * @access public
     */
    function parseHeader($header) {
        if (preg_match("/^HTTP\/((\d)\.(\d))\40*(\d*)\40*(.*)[\40\t]*/",$header,$array1)) {
            return array(code =>$array1[4], version=>$array1[1], majorversion => $array[2], minorversion => $array[3], message => $array[5]);
        }
        if (preg_match("/^([\w-]+?)\:[\40\t]*(.+)[\40\t]*/",$header,$array)) {
            $key=strtolower($array[1]);
            return array("key" => $key, value => $value);
        }
        else {
            return false;
        }
    }

    /**
     * Check a URI
     *
     * @param string The URI
     *
     * @return mixed Returns parse_url'ed array on valid URI, or a PEAR_Error on failure
     * @access private
     */
    function checkuri($uri) {
        $array = parse_url($uri);
        if (!$array["scheme"]) return 11;
        switch (strtolower($array["scheme"])) {
            case http:
                break;
            default:
                return 12;
        }
        if (!$array["host"]) return 10;
        if (!$array["port"]) {
            switch (strtolower($array["scheme"])) {
                case http:
                    $array["port"]=80;
            }
        }
        if (!$array["path"]) $array["path"]="/";
        return $array;
    }

    /**
     * Attempt to connect to the web server.
     *
     * @param string The host to connect to
     * @param int The port to use.
     *
     * @return mixed Returns a PEAR_Error with an error message on any
     *               kind of failure, or true on success.
     * @access private
     */
    function connect($host,$port=80) {
        include_once('Socket.php');
        $this->socket = new Net_Socket();
        if (PEAR::isError($this->socket->connect($host, $port))) {
            return new PEAR_Error("Couldn't connect");
        }
        return true;
    }

    /**
     * Throw an exception
     * 0 - unknown error
     * 1-99 syntax error
     * 100-599 HTTP error code. Do not use these errors for anything else
     * 700-800 Non-HTTP network error
     * 
     * @param int Error code. An error code indicating the reason for failure. Error codes are named thus: 
     * @param array Error parameters. Replaceable parameters go here.
     *
     * @return object Returns a PEAR_Error at present, although $this method is designed to be customized
     * @access private
     */
    function raiseError($errorcode,$errortext=array()) {
        $httpcodes=array(100 => 'Continue', // Taken from libwww perl
        101 => 'Switching Protocols',
        102 => 'Processing',                      // WebDAV
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        207 => 'Multi-Status',                    // WebDAV
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found',
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        307 => 'Temporary Redirect',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Large',
        415 => 'Unsupported Media Type',
        416 => 'Request Range Not Satisfiable',
        417 => 'Expectation Failed',
        422 => 'Unprocessable Entity',            // WebDAV
        423 => 'Locked',                          // WebDAV
        424 => 'Failed Dependency',               // WebDAV
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported',
        507 => 'Insufficient Storage'            // WebDAV
        );
        if ($errorcode>=100 && $errorcode<600) {
            if (!isset($httpcodes[$errorcode])) { // Unknown HTTP error
                $message="Unknown HTTP error: code $errorcode";
                $errorcode=0;
            }
            else {
                $message="HTTP error ".$httpcodes[$errorcode];
            }
        }
        else {
            switch ($errorcode) {
                case 10:
                    $message="Bad URI: missing host";
                    break;
                case 11:
                    $message="Bad URI: missing URI scheme";
                case 12:
                    $message="Bad URI: unsupported URI scheme";
                    break;
                case 20:
                    $message="Unsupported HTTP request type: $errortext[0]";
                    break;
                case 601:
                    $message='Unable to open socket: $errortext[0]:$errortext[1]';
                    break;
                case 602:
                    $message='Unable to write to socket: $errortext[0]:$errortext[1]';
                    break;
                case 603:
                    $message='Unable to read from socket: $errortext[0]:$errortext[1]';
                    break;
            }
        }
        return new PEAR_Error($message,$errorcode);
    }
}
?>