<?php
define('idDiGIR_sock_http','$Id: sock_http.php,v 1.2 2003/05/06 20:33:23 vieglais Exp $');
if (defined('idDIGIR_VERSION_REQUEST'))
    return;
/*
$Id: sock_http.php,v 1.2 2003/05/06 20:33:23 vieglais Exp $

This library provide a simple implementation of multiple simultaneous
xml parsers loading from HTTP sockets.  The maximum number of simultanteous
sockets can be limited.  Processing is non-blocking, though the duration of the
entire operation is limited by the duration of the slowest connection.


== License ==
Copyright (c) 2002 The University of Kansas Natural History Museum and
Biodiversity Research Center.  All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
with the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

*  Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimers.

* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.

* Neither the names of The University of Kansas Natural History Museum and
Biodiversity Research Center, The University of Kansas at Lawrence, nor the
names of its contributors may be used to endorse or promote products derived
from this Software without specific prior written permission.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
////////////////////////////////////////////////////////////
/**
* @class xmlTarget
*
* Implements an xml parser that reads content from a socket connection
* and parses the information as it is retrieved.
*
*/
class xmlTarget
{
    var $host;
    var $port;
    var $path;
    var $sendData;
    var $addr;
    var $errors;
    var $writedata;
    var $write;
    var $read;
    var $startcontent;
    var $id;
    var $buffer;
    var $done;
    var $parser;
    var $socketID;
    var $socket;
    var $readLength;
    var $nameSpaces;

    /**
    * Create a new XML target.
    * @param string $id Identifier for this target
    * @param string $host The host name of this target
    * @param int $port The TCPIP port number to connect on
    * @param string $path The HTTP path of the request
    * @param string $data Data to be sent
    */
    function xmlTarget($id,$host,$port,$path,$data)
    {
        $this->id = $id;
        $this->host = $host;
        $this->port = $port;
        $this->path = $path;
        $this->sendData = $data;
        $this->done = TRUE;
        $this->addr = NULL;
        $this->errors = array();
        $this->writedata = '';
        $this->write = FALSE;
        $this->read = FALSE;
        $this->startcontent = FALSE;
        $this->buffer = '';
        $this->parser = NULL;
        $this->socketID = '';
        $this->socket = NULL;
        $this->readLength = 4096;
        $this->nameSpaces = array();

        $addr = @gethostbyname($this->host);
        if (ip2long($addr) == -1)
        {
            //could not resolve host
            $this->done = TRUE;
            array_push($this->errors,"Could not resolve host name $host");
        }
        else
        {
            $this->addr = $addr;
            $this->done = FALSE;
            $this->writedata = "GET ".$this->path."?".$this->sendData." HTTP/1.0\r\nHost: ".
                    $this->host."\r\nUser-Agent: DiGIR/0.0\r\n\r\n";
        }
   }

   function addNameSpace($name,$space)
   {
//        foreach ($this->nameSpaces as $n=>$s)
//        {
//            if ($space == $s)
//                return;
//        }
        $this->nameSpaces[$name] = $space;
   }

   /**
   * Initialize the parser.  There should really be no reason to call this
   * method as it is called by connect()
   */
   function initParser()
   {
        //$this->parser = xml_parser_create_ns();
        $this->parser = xml_parser_create();
        xml_set_object($this->parser, $this);
        xml_parser_set_option($this->parser,XML_OPTION_CASE_FOLDING,0);
        xml_set_element_handler($this->parser, '_startelement', '_endelement');
        xml_set_character_data_handler($this->parser,'_cdata');
        xml_set_processing_instruction_handler($this->parser,'_pidata');
        xml_set_default_handler($this->parser,'_defaulthandler');
        xml_set_unparsed_entity_decl_handler($this->parser,'_updecl');
        xml_set_notation_decl_handler($this->parser,'_notdecl');
        xml_set_external_entity_ref_handler($this->parser,'_extref');
        //xml_set_start_namespace_decl_handler($this->parser,'_namespace');
   }

   /**
   * Parses a chunk of data.
   * @param string $data The next chunk of the xml stream to parse
   * @param boolean $end TRUE if this is the last chunk of data
   * @return boolean TRUE if no errors encoutnered, otherwise FALSE
   */
    function parse($data,$end)
    {
        if ($this->parser === NULL)
            return TRUE;
        if (!xml_parse($this->parser, $data, $end))
        {
            array_push($this->errors,
                sprintf("XML (".$this->id.") error: %s at line %d",
                xml_error_string(xml_get_error_code($this->parser)),
                xml_get_current_line_number($this->parser)));
            xml_parser_free($this->parser);
            $this->parser = NULL;
            return FALSE;
        }
        if ($end)
        {
            xml_parser_free($this->parser);
            $this->parser = NULL;
        }
        return TRUE;
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _startelement($parser, $name, $attrs)
    {
        echo "Name (".$this->id.") = $name \n";
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _endelement($parser, $name)
    {
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _cdata($parser,$data)
    {
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _pidata($parser,$target,$pidata)
    {
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _defaulthandler($parser,$data)
    {
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _updecl($parser,
                     $entity_name,
                     $base,
                     $system_id,
                     $public_id,
                     $notation_name)
    {
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _notdecl($parser, $notation_name, $base, $system_id, $public_id)
    {
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _extref($parser, $open_entity_names, $base, $system_id, $public_id)
    {
        return 1;
    }

    /**
    * See the xml_xxx methods in PHP documentation.  You may need to
    * override this method in a derived class for anything interesting
    * to happen.
    */
    function _namespace($parser,$name,$space)
    {
        echo "namespace: $name : $space\n";
    }
    
    /**
    * Sets the socketID parameter accoring to the resource ID of the socket.
    * This is done so that a container can associate an instance of this
    * class with an open socket.
    *
    * @param socket $socket The socket with which to be associated
    */
    function setSocket(&$socket)
    {
        if ($socket !== NULL)
        {
            $tmp = str_replace(" ","_",$socket);
            $this->socketID = $tmp;
        }
        else
            $this->socketID = '';
    }
    
    /**
    * Creates a new socket and attempts to open the connection.
    * @return boolean The result of attempting to open the socket.
    * If an error occurs, then the reason is appended to the errors array of
    * this object.
    */
    function connect()
    {
        $this->socket = socket_create(AF_INET,SOCK_STREAM,0);
        $res = @socket_connect($this->socket,$this->addr,$this->port);
        if ($res)
        {
            $this->setSocket($this->socket);
            $this->initParser();
        }
        else
        {
            $err = socket_last_error($this->socket);
            $serr = socket_strerror($err);
            array_push($this->errors,$serr);
            $this->done = TRUE;
            $this->socket = NULL;
        }
        return $res;
    }
    
    /**
    * Sends the data.  Does nothing if the data has already been sent.
    * @return boolean FALSE if a socket error occurred, otherwise TRUE
    */
    function writeData()
    {
        if ($this->write)
            return TRUE;
        if ($this->done)
            return TRUE;
        if (count($this->errors) > 0)
            return TRUE;
        $tmp = $this->writedata;
        $res = socket_write($this->socket,$tmp,strlen($tmp));
        if ($res === FALSE)
        {
            $err = socket_last_error($this->socket);
            array_push($this->errors(socket_strerror($err)));
            socket_clear_error($this->socket);
            return FALSE;
        }
        else
            $this->write = TRUE;
        return TRUE;
    }
    
    /**
    * Reads data from the socket, and if the start of content has been
    * reached, sends the chunk of content to the parser.
    * @return boolean TRUE is no socket or parse errors occurred, otherwise
    * FALSE.
    */
    function readData()
    {
        if ($this->read)
            return TRUE;
        if ($this->done)
            return TRUE;
        if (count($this->errors) > 0)
        {
            $this->done = TRUE;
            return TRUE;
        }
        $res = TRUE;
        if ($tmp = socket_read($this->socket,$this->readLength,PHP_BINARY_READ))
        {
            //have we started processing the response content yet?
            if ($this->startcontent)
            {
                $res = $this->parse($tmp,FALSE);
                $this->buffer .= $tmp;
            }
            else
            {
                //look for the start of content = right after
                //first occurence of \r\n\r\n
                $this->buffer .= $tmp;
                $chk = $this->buffer;
                $cpos = strpos($chk,"\r\n\r\n");
                if ($cpos !== FALSE)
                {
                    $tmp = substr($chk,$cpos + strlen("\r\n\r\n"));
                    $tmp = ltrim($tmp);
                    $this->buffer = $tmp;
                    $this->startcontent = TRUE;
                    $res = $this->parse($tmp,FALSE);
                }
            }
        }
        else
        {
            //no more data to read
            $tmp = '';
            $res = $this->parse($tmp,TRUE);
            $this->read = TRUE;
            $this->done = TRUE;
            
            //remove our socket ID
            socket_close($this->socket);
            $this->socket = NULL;
            $this->setSocket($this->socket);
        }
        return $res;
    }
    
    /*
    * Allows loading of the resource independently of the multiple
    * sockets interface.
    */
    function Load()
    {
        $URL = "http://".$this->host.":".$this->port;
        $URL .= $this->path;
        if (strlen($this->sendData) > 0)
        {
            $URL .= "?".$this->sendData;
        }
        $fh = @fopen($URL,"r");
        if (!fh)
            return FALSE;

        $this->initParser();
        while ($buf = fread($fh,4096))
        {
            $this->parse($buf,feof($fh));
        }
        $this->read = TRUE;
        $this->done = TRUE;
        return TRUE;
    }
    
}
////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////
/**
* Simple derived class to check that the idea work ok
*/
class xmlTargetX extends xmlTarget
{
    function _startelement($parser, $name, $attrs)
    {
        echo "XXX (".$this->id.") = $name \n";
        flush();
    }

}
////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////
/**
* Implements multiple simultaneous XML parsers from socket sources.
*/
class multiTarget
{
    var $targets;
    var $maxSockets;
    var $timeout;
    var $nameSpaces;
    
    /**
    * Constructor for this class.
    * maxSockets also determines the maximum number of jobs, and hence
    * the number of xml parsers in oepration at once.  Not sure what the upper
    * limit of this value can be.
    *
    * @param int $maxSockets The maximum number of simultaneous jobs
    * @param int $timeout The timeout value for socket_select()
    */
    function multiTarget($maxSockets = 255, $timeout = 0)
    {
        $this->maxSockets = $maxSockets;
        $this->timeout = $timeout;
        $this->targets = array();
        $this->nameSpaces = array();
    }
    
    /**
    * Adds a new target to the list of targets (jobs).
    * @param xmlTarget $target A reference to an instance of an object derived
    * from xmlTarget.
    */
    function addTarget(&$target)
    {
        $res = array_push($this->targets,$target)-1;
        return $res;
    }
    
    function &getTarget($index)
    {
        if (isset($this->targets[$index]))
            return $this->targets[$index];
        return null;
    }
    
    /**
    * Internal method used to locate a target by its user id value.
    */
    function findTarget($id)
    {
        foreach ($this->targets as $k=>$t)
        {
            if ($t->id == $id)
                return $k;
        }
        return NULL;
    }
    
    /**
    * Internal method used to locate a target by its socket resource identifier
    * @param socket $socket Reference to a socket associated with a target
    * @return key The key to locate the target if found, otherwise NULL
    */
    function _findTargetSID(&$socket)
    {
        $tmp = str_replace(" ","_",$socket);
        foreach ($this->targets as $k=>$t)
        {
            if ($t->socketID == $tmp)
                return $k;
        }
        return NULL;
    }
    
    /**
    * Determines if all the targets have completed their operations
    * @return boolean TRUE if all done, otherwie FALSE
    */
    function _checkAllDone()
    {
        foreach ($this->targets as $t)
        {
            if ($t->done == FALSE)
                return FALSE;
        }
        return TRUE;
    }

    /**
    * Creates an array which contains copies of the currently opened sockets.
    * The resulting array can be fed to socket_select()
    *
    * @return array Array of sockets
    */
    function _copySocks()
    {
        $res = array();
        foreach ($this->targets as $t)
        {
            if ($t->socket !== NULL)
            {
                array_push($res,$t->socket);
            }
        }
        return $res;
    }
    
    /**
    * Counts the number of jobs currently in progress.
    *
    * @return int The number of open jobs.
    */
    function countOpenJobs()
    {
        $njobs = 0;
        foreach ($this->targets as $t)
        {
            if ($t->socket !== NULL)
            {
                $njobs ++;
            }
        }
        return $njobs;
    }
    
    /**
    * Counts the number of jobs that have not been completed.  This may
    * include currently open jobs.
    *
    * @return int The number of jobs that have not been completed.
    */
    function countJobsToDo()
    {
        $njobs = 0;
        foreach ($this->targets as $t)
        {
            if (!$t->done)
            {
                $njobs ++;
            }
        }
        return $njobs;
    }
    
    /**
    * Starts the jobs and continues until they are completed.
    *
    */
    function doJob()
    {
        //count the number of tasks to be done
        $Done = $this->_checkAllDone();
        $tkeys = array_keys($this->targets);
        while (!$Done)
        {
            //should we try and start the next job?
            $openJobCount = $this->countOpenjobs();
            $jobsRemaining = $this->countJobsToDo();
            reset($tkeys);
            $tk = current($tkeys);
            while (($jobsRemaining > 0)
                && ($openJobCount < $this->maxSockets)
                && ($tk !== FALSE))
            {
                //setup the next connection for work
                if ((!$this->targets[$tk]->done)
                    && ($this->targets[$tk]->socket === NULL))
                {
                    $result = $this->targets[$tk]->connect();
                    if ($result)
                    {
                        $openJobCount ++;
                        $jobsRemaining --;
                    }
                }
                $tk = next($tkeys);
            }


            //send data on the socket
            $ws = $this->_copySocks();
            socket_select($read = NULL,$ws, $except = NULL, $this->timeout);
            foreach ($ws as $k=>$s)
            {
                //resolve the target given the socket
                $tk = $this->_findTargetSID($s);
                if ($tk !== NULL)
                {
                    $res = $this->targets[$tk]->writeData();
                }
            }
            
            //receive data on the socket
            $ws = $this->_copySocks();
            socket_select($ws,$write = NULL, $except = NULL, $this->timeout);
            foreach ($ws as $k=>$s)
            {
                $tk = $this->_findTargetSID($s);
                if ($tk !== NULL)
                {
                    $res = $this->targets[$tk]->readData($s);
                }
            }

            //Check to see if we need to continue
            if (connection_aborted())
            {
                foreach ($this->targets as $t)
                    $t->done = TRUE;
            }

            //check if there is anything left to do
            $Done = $this->_checkAllDone();
            
        }
        
        //add all the collected namespaces
        foreach ($this->targets as $target)
        {
            foreach ($target->nameSpaces as $name => $space)
            {
                $this->addNameSpace($name,$space);
            }
        }
    }

   function addNameSpace($name,$space)
   {
/*        foreach ($this->nameSpaces as $n=>$s)
        {
            if ($space == $s)
                return;
        }*/
        $this->nameSpaces[$name] = $space;
   }
}

/*
set_magic_quotes_runtime(0);
dl('php_sockets.dll');
error_reporting(E_ALL);

$targets = new multiTarget();
$target = new xmlTargetX('1',
                         '128.32.214.123',
                         80,
                         '/phpProvider/DiGIR.php?operation=metadata','');
$targets->addTarget($target);
$target = new xmlTarget('2',
                        'velo.dyndns.info',
                        80,
                        '/phpdev/digir/digir.php','');
$targets->addTarget($target);
$target = new xmlTarget('3',
                        'digir.sourceforge.net',
                        80,
                        '/test/phpProvider/DiGIR.php','');
$targets->addTarget($target);

$targets->doJob();
*/
?>
