<?php
/*
+---------------------------------------------------------------------------
|   PHP-IRC v2.2.1 Service Release
|   ========================================================
|   by Manick
|   (c) 2001-2005 by http://www.phpbots.org/
|   Contact: manick@manekian.com
|   irc: #manekian@irc.rizon.net
|   ========================================
+---------------------------------------------------------------------------
|   > socket module
|   > Module written by Manick
|   > Module Version Number: 2.2.0
+---------------------------------------------------------------------------
|   > This program is free software; you can redistribute it and/or
|   > modify it under the terms of the GNU General Public License
|   > as published by the Free Software Foundation; either version 2
|   > of the License, or (at your option) any later version.
|   >
|   > This program is distributed in the hope that it will be useful,
|   > but WITHOUT ANY WARRANTY; without even the implied warranty of
|   > MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
|   > GNU General Public License for more details.
|   >
|   > You should have received a copy of the GNU General Public License
|   > along with this program; if not, write to the Free Software
|   > Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+---------------------------------------------------------------------------
|   Changes
|   =======-------
|   > If you wish to suggest or submit an update/change to the source
|   > code, email me at manick@manekian.com with the change, and I
|   > will look to adding it in as soon as I can.
+---------------------------------------------------------------------------
*/

class socket {

	private $rawSockets; //array of raw sockets to be used by select
	private $socketInfo; //index by intval($rawSockets[socket])
	private $numSockets; //number of sockets currently in use
	private $writeSocks; //sockets that have write buffers queued
	private $numWriteSocks;

	private $readQueueSize = 0;

	private $tcpRangeStart = 1025;
	private $timeoutSeconds = 0;
	private $timeoutMicroSeconds = 0;
	
	private $myTimeout = 0;

	private $procQueue;

	public function __construct()
	{
		$this->connectSockets = array();
		$this->rawSockets = array();
		$this->socketInfo = array();
		$this->writeSocks = array();
		$this->readQueueSize = 0;
		$this->numSockets = 0;
		$this->numWriteSocks = 0;
	}

	public function setProcQueue($class)
	{
		$this->procQueue = $class;
	}

	public function getNumSockets()
	{
		return $this->numSockets;
	}

	public function setTcpRange($range)
	{
		if (intval($range) != 0)
		{
			$this->tcpRangeStart = $range;
		}
	}

	public function getHost($sockInt)
	{
		$status = socket_getsockname($this->socketInfo[$sockInt]->socket, $addr);

		if ($status == false)
		{
			return false;
		}

		return $addr;

	}
	
	public function getRemoteHost($sockInt)
	{
		$status = socket_getpeername($this->socketInfo[$sockInt]->socket, $addr);

		if ($status == false)
		{
			return false;
		}

		return $addr;

	}

    public function setTimeout($time)
    {
		$sec = intval($time);
		$msec = intval(($time - $sec)*1e6);

        if ($sec == 0)
		{
			$msec = $msec < $this->myTimeout ? $this->myTimeout : $msec;
		}

		if ($sec < $this->timeoutSeconds)
		{
			$this->timeoutSeconds = $sec;
			$this->timeoutMicroSeconds = $msec;
		}
		else if ($sec == $this->timeoutSeconds)
		{
			if ($msec < $this->timeoutMicroSeconds)
			{
				$this->timeoutMicroSeconds = $msec;
			}
		}
	}
	
	public function setHandler($sockInt, $owner, $class, $function)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return false;
		}

		$sock = $this->socketInfo[$sockInt];

		$sock->owner = $owner;
		$sock->class = $class;
		$sock->func = $function;
		
		return true;
	}

	/* For debug... */
	public function showSocks($read, $write)
	{
		echo "\n\nRead:\n";
		if (is_array($read))
		{
			foreach($read AS $sock)
			{
				echo $sock . "\n";
			}
		}
		echo "\nWrite:\n";
		if (is_array($write))
		{
			foreach($write AS $sock)
			{
				echo $sock . "\n";
			}
		}
		echo "\n";
	}

	public function handle()
	{
		//For debug
		//echo "Read: " . $this->readQueueSize . " Write: " . $this->writeQueueSize . "\n";
		//echo "timeout: " . $this->timeoutSeconds . "-" . $this->timeoutMicroSeconds . "\n";

		if ($this->numSockets < 1)
		{
			if ($this->timeoutSeconds > 0)
			{
				sleep($this->timeoutSeconds);
			}
			
			if ($this->timeoutMicroSeconds > 0)
			{
				usleep($this->timeoutMicroSeconds);
			}
			
			$this->timeoutSeconds = 1000;
			return;
		}

		if ($this->numSockets < 1)
		{
			$sockArray = NULL;
			$except = NULL;
		}
		else
		{
			$sockArray = $this->rawSockets;
			$except = $this->rawSockets;
		}

		if ($this->numWriteSocks < 1)
		{
			$writeArray = NULL;
		}
		else
		{
			$writeArray = $this->writeSocks;
		}

		//For debug
		//$this->showSocks($sockArray, $writeArray);

		$newData = socket_select($sockArray, $writeArray, $except, $this->timeoutSeconds, $this->timeoutMicroSeconds);

		$this->timeoutSeconds = 1000;

		if ($newData === false)
		{
			die("socket_select error"); // need to change this to handle errors
			return;
		}

		if (!$newData)
		{
			return;
		}

		if (count($sockArray) != 0)
		{
			foreach($sockArray AS $socket)
			{
				$sockIntval = intval($socket);

				switch($this->socketInfo[$sockIntval]->status)
				{
					case SOCK_CONNECTED:
						$this->readSocket($sockIntval);
						break;
					case SOCK_LISTENING:
						$this->acceptSocket($sockIntval);
						break;
					case SOCK_CONNECTING:
						$this->connectSocket($sockIntval);
						break;
					default:
						break;
				}
			}

		}

		if (count($writeArray) != 0)
		{
			foreach ($writeArray AS $socket)
			{
				$sockIntval = intval($socket);
				$this->sendSocketQueue($sockIntval, 1);
			}
		}

		if (count($except) != 0)
		{
			foreach($except AS $socket)
			{
				$sockIntval = intval($socket);
				$this->markDead($sockIntval);
			}
		}

	}


	private function callBack($sockIntval, $msg)
	{
		//Schedule the callback to run
		if ($this->socketInfo[$sockIntval]->func != "" &&
			$this->socketInfo[$sockIntval]->func != null)
		{
			$this->procQueue->addQueue(	$this->socketInfo[$sockIntval]->owner,
										$this->socketInfo[$sockIntval]->class,
										$this->socketInfo[$sockIntval]->func,
										$msg,
										.01);
		}
	}

	private function readSocket($sockIntval)
	{

		if ($this->isDead($sockIntval))
		{
			return;
		}
		
		if ($this->socketInfo[$sockIntval]->status != SOCK_CONNECTED)
		{
			return;
		}

		$dataRead = false;

		$socket = $this->socketInfo[$sockIntval]->socket;
		//Read in 4096*30 bytes

		for ($i = 0; $i < 30; $i++)
		{
			$response = @socket_read($socket, 8192, PHP_BINARY_READ);

			$respLength = strlen($response);

			if ($response === false)
			{
				$err = socket_last_error($this->socketInfo[$sockIntval]->socket);

				if ($err != EALREADY && $err != EAGAIN && $err != EINPROGRESS)
				{
					$this->markDead($sockIntval);
				}
				break;
			}
			else if ($respLength === 0)
			{
				if ($i == 0)
				{
					$this->markDead($sockIntval);
				}
				break;
			}

			$dataRead = true;

			$this->readQueueSize += $respLength;
			$this->socketInfo[$sockIntval]->readLength += $respLength;
			$this->socketInfo[$sockIntval]->readQueue .= $response;
			
		}
		
		if ($dataRead == true)
		{
			if ($this->socketInfo[$sockIntval]->readScheduled == false)
			{
				$this->callBack($sockIntval, CONN_READ);
				$this->socketInfo[$sockIntval]->readScheduled = true;
			}
		}
	}


	private function sendSocketQueue($sockIntval, $queued = 0)
	{
		$socket = $this->socketInfo[$sockIntval]->socket;

		if ($this->isDead($sockIntval))
		{
			return;
		}
		
		if (($bytesWritten = @socket_write($socket, $this->socketInfo[$sockIntval]->writeQueue)) === false)
		{

			$socketError = socket_last_error($socket);

			switch ($socketError)
			{
				case EAGAIN:
				case EALREADY:
				case EINPROGRESS:
					break;
				default:
					$this->markDead($sockIntval);
					break;
			}

		}
		else
		{
			$this->socketInfo[$sockIntval]->writeQueue = substr($this->socketInfo[$sockIntval]->writeQueue, $bytesWritten);
			$this->socketInfo[$sockIntval]->writeLength -= $bytesWritten;

			//Queue Empty, Remove socket from write
			if ($this->socketInfo[$sockIntval]->writeLength == 0 && $queued == 1)
			{
				$this->removeWriteSocketFromArray($sockIntval);
			}
			
			if ($this->socketInfo[$sockIntval]->writeLength == 0)
			{
				unset($this->socketInfo[$sockIntval]->writeQueue);
				$this->socketInfo[$sockIntval]->writeQueue = "";
			}

			//Callback after we wrote to socket
			
			if ($this->socketInfo[$sockIntval]->writeScheduled == false)
			{
				$this->callBack($sockIntval, CONN_WRITE);
				$this->socketInfo[$sockIntval]->writeScheduled = true;
			}

		}

	}

	private function createSocket()
	{
		$socket = socket_create(AF_INET, SOCK_STREAM, 0);

		socket_set_option($socket,SOL_SOCKET,SO_REUSEADDR,1);

		if ($socket == false)
		{
			return false;
		}

		socket_clear_error($socket);

		if (socket_set_nonblock($socket) == false)
		{
			@socket_close($socket);
			return false;
		}

		return $socket;
	}

	public function bindIP($sockInt, $ip)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return;
		}

		$sockData = $this->socketInfo[$sockInt];
		
		if ($sockData->status != SOCK_CONNECTING)
		{
			return;
		}

		socket_bind($sockData->socket, $ip);
	}

	public function addSocket($host, $port)
	{
		$listening = false;

		if ($host == null)
		{
			$host = false;
			$listening = true;
		}

		$socket = $this->createSocket();

		if ($socket == false)
		{
			return false;
		}

		if ($listening == true)
		{
			$boundError = false;
			$currentPort = $this->tcpRangeStart;

			if ($port !== null)
			{
				$boundError = @socket_bind($socket, 0, $port);
				if ($boundError === false)
				{
					return false;
				}
			}
			else
			{
				while ($boundError === false)
				{
					$boundError = @socket_bind($socket, 0, $currentPort);
					$currentPort++;
	
					if ($currentPort > $this->tcpRangeStart + HIGHEST_PORT)
					{
						return false;
					}
				}

				$port = $currentPort - 1;
			}


			if (socket_listen($socket) === false)
			{
				return false;
			}
			
			
			if (DEBUG == 1)
			{
				echo "Socket Listening: " . intval($socket) . "\n";
			}


		}
		else
		{
			if (DEBUG == 1)
			{
				echo "Socket Opened: " . intval($socket) . "\n";
			}
		}
		
		$newSock = new socketInfo;

		$newSock->socket = $socket;
		$newSock->owner = null;
		$newSock->class = null;
		$newSock->func = null;
		$newSock->readQueue = "";
		$newSock->writeQueue = "";
		$newSock->readLength = 0;
		$newSock->writeLength = 0;
		$newSock->host = $host;
		$newSock->port = $port;
		$newSock->newSockInt = array();
		$newSock->readScheduled = false;
		$newSock->writeScheduled = false;

		$this->socketInfo[intval($socket)] = $newSock;

		if ($listening == true)
		{
			$newSock->status = SOCK_LISTENING;
		}
		else
		{
			$newSock->status = SOCK_CONNECTING;
		}

		$this->numSockets++;
		$this->rawSockets[] = $socket;

		return intval($socket);
	}
	
	public function clearReadSchedule($sockInt)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return false;
		}

		$this->socketInfo[$sockInt]->readScheduled = false;

		return true;
	}

	public function clearWriteSchedule($sockInt)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return false;
		}

		$this->socketInfo[$sockInt]->writeScheduled = false;

		return true;
	}

/*
	public function beginConnect($sockInt)
	{
		$this->procQueue->addQueue(null, $this, "connectSocketProcess", $sockInt, 0);
	}
*/

	//process to connect the socket $sockInt
	public function connectSocketTimer($sockInt)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return false;
		}

		if ($this->socketInfo[$sockInt]->status != SOCK_CONNECTING)
		{
			return false;
		}

		$this->connectSocket($sockInt);

		return true;
	}


	//Remove all sockets from a specific irc bot
	public function removeOwner($class)
	{
		foreach($this->socketInfo AS $sockInt => $data)
		{
			if ($class === $data->owner)
			{
				$this->killSocket($sockInt);
				$this->removeSocket($sockInt);
			}
		}
	}

	public function killSocket($sockInt)
	{
		if (DEBUG == 1)
		{
			echo "Killing socket: " . $sockInt . "\n";
		}
		
		if ($this->socketInfo[$sockInt]->status == SOCK_ACCEPTED)
		{
			$this->acceptedSockets--;
		}

		if ($this->socketInfo[$sockInt]->status != SOCK_DEAD)
		{
			$this->removeReadSocketFromArray($sockInt);
			$this->removeWriteSocketFromArray($sockInt);
			$this->socketInfo[$sockInt]->status = SOCK_DEAD;
		}

		if (is_resource($this->socketInfo[$sockInt]->socket))
		{
			if (DEBUG == 1)
			{
				echo "Closed socket: " . $sockInt . "\n";
			}

			socket_clear_error($this->socketInfo[$sockInt]->socket);
			socket_close($this->socketInfo[$sockInt]->socket);
		}
		else
		{
			if (DEBUG == 1)
			{
				echo "Socket already closed: " . $sockInt . "\n";
			}
		}
	}

	public function removeSocket($socketIntval)
	{

		$this->readQueueSize -= $this->socketInfo[$socketIntval]->readLength;

		unset($this->socketInfo[$socketIntval]->class);
		unset($this->socketInfo[$socketIntval]->owner);
		unset($this->socketInfo[$socketIntval]);
	}

	private function removeReadSocketFromArray($socketIntval)
	{
		foreach ($this->rawSockets AS $index => $socket)
		{
			if ($socket === $this->socketInfo[$socketIntval]->socket)
			{
				unset($this->rawSockets[$index]);
				$this->numSockets--;
				break;
			}
		}
	}

	private function removeWriteSocketFromArray($socketIntval)
	{
	
		foreach ($this->writeSocks AS $index => $rawSocket)
		{
			if ($rawSocket === $this->socketInfo[$socketIntval]->socket)
			{
				unset($this->writeSocks[$index]);
				$this->numWriteSocks--;
				break;
			}
		}	
	}

	public function sendSocket($sockInt, $data)
	{
		if ($this->isDead($sockInt))
		{
			return;
		}

		if ($this->socketInfo[$sockInt]->status != SOCK_CONNECTED)
		{
			return;
		}

		$inQueue = $this->socketInfo[$sockInt]->writeLength > 0 ? true : false;

		$len = strlen($data);
		$this->socketInfo[$sockInt]->writeQueue .= $data;
		$this->socketInfo[$sockInt]->writeLength += $len;

		if (!$inQueue)
		{
			$this->sendSocketQueue($sockInt, 0);

			if ($this->socketInfo[$sockInt]->status == SOCK_CONNECTED)
			{
				if ($this->socketInfo[$sockInt]->writeLength > 0)
				{
					$this->writeSocks[] = $this->socketInfo[$sockInt]->socket;
					$this->numWriteSocks++;
				}
			}
		}

		return $len;
	}

	private function acceptSocket($sockInt)
	{
		$sockData = $this->socketInfo[$sockInt];
		
		$newSock = @socket_accept($sockData->socket);
		socket_set_nonblock($newSock);

		if ($newSock === false)
		{
			return false;
		}

		$newSockInt = intval($newSock);

		if (DEBUG == 1)
		{
			echo "Accepted new connection on: " . $newSockInt . "\n";
		}


		$this->socketInfo[$newSockInt] = clone $sockData;
		$this->socketInfo[$newSockInt]->socket = $newSock;
		$this->socketInfo[$newSockInt]->status = SOCK_ACCEPTING; /* fix a onRead done before onAccept */

		$this->numSockets++;
		$this->rawSockets[] = $newSock;

		$this->socketInfo[$sockInt]->newSockInt[] = $newSockInt;

		//Schedule the callback to run
		$this->callBack($sockInt, CONN_ACCEPT);

		return true;
	}

	private function connectSocket($sockInt)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return;
		}
		
		if ($this->socketInfo[$sockInt]->status == SOCK_CONNECTED)
		{
			return;
		}

		if (@socket_connect($this->socketInfo[$sockInt]->socket,
							$this->socketInfo[$sockInt]->host,
							$this->socketInfo[$sockInt]->port) === true)
		{
			$this->socketInfo[$sockInt]->status = SOCK_CONNECTED;
			$this->callBack($sockInt, CONN_CONNECT);
		}
		else
		{
			$socketError = socket_last_error($this->socketInfo[$sockInt]->socket);

			switch ($socketError)
			{
				case 10022:
					if (OS != 'windows')
					{
						$this->markDead($sockInt);
					}
					break;
				case EISCONN:
					$this->socketInfo[$sockInt]->status = SOCK_CONNECTED;
					$this->callBack($sockInt, CONN_CONNECT);
					break;
				case EAGAIN:
				case EALREADY:
				case EINPROGRESS:
					break;
				default:
					$this->markDead($sockInt);
					break;
			}
		}

		return;
	}

	public function getSockStatus($sockInt)
	{
		return (isset($this->socketInfo[$sockInt]) ? $this->socketInfo[$sockInt]->status : false);
	}

	public function getSockData($sockInt)
	{
		return (isset($this->socketInfo[$sockInt]) ? $this->socketInfo[$sockInt] : false);
	}

	public function alterSocket($sockInt, $level, $opt, $val)
	{
		return socket_set_option($this->socketInfo[$sockInt]->socket, $level, $opt, $val);
	}

	public function getSockError($sockInt)
	{
		return socket_last_error($this->socketInfo[$sockInt]->socket);
	}

	public function getSockStringError($sockInt)
	{
		$strErr = "[" . self::getSockError($sockInt) . "]:" . socket_strerror(socket_last_error($this->socketInfo[$sockInt]->socket));
		$strErr = str_replace("\n", "", $strErr);
		return $strErr;
	}

	public function hasAccepted($sockInt)
	{
		if (!isset($this->socketInfo[$sockInt]))
		{
			return false;
		}

		$newSockInt = array_shift($this->socketInfo[$sockInt]->newSockInt);
		
		$this->socketInfo[$newSockInt]->status = SOCK_CONNECTED;

		return $newSockInt;
	}

	private function markDead($sockInt)
	{
		if (DEBUG == 1)
		{
			echo "Marking socket dead: " . $sockInt . "\n";
		}

		$this->removeReadSocketFromArray($sockInt);
		$this->removeWriteSocketFromArray($sockInt);
		$this->socketInfo[$sockInt]->status = SOCK_DEAD;
		$this->callBack($sockInt, CONN_DEAD);
	}

	public function isDead($sockInt)
	{
		$socket = $this->socketInfo[$sockInt]->socket;

		if (!is_resource($socket))
		{
			$this->markDead($sockIntval);
			return true;
		}

		if (!isset($this->socketInfo[$sockInt]))
		{
			return true;
		}

		switch ($this->socketInfo[$sockInt]->status)
		{
			case SOCK_DEAD:
				return true;
				break;
			default:
				return false;
				break;
		}
	}

	public function hasWriteQueue($sockInt)
	{
		if ($this->socketInfo[$sockInt]->writeLength > 0)
		{
			return $this->socketInfo[$sockInt]->writeLength;
		}
		else
		{
			return false;
		}
	}

	public function getQueue($sockInt)
	{
		$this->readQueueSize -= $this->socketInfo[$sockInt]->readLength;
		$queue = $this->socketInfo[$sockInt]->readQueue;
		unset($this->socketInfo[$sockInt]->readQueue);
		$this->socketInfo[$sockInt]->readQueue = "";
		$this->socketInfo[$sockInt]->readLength = 0;
		return $queue;
	}

	public function hasQueue($sockInt)
	{
		if ($this->socketInfo[$sockInt]->readLength > 0)
		{
			return true;
		}

		return false;
	}


	public function hasLine($sockInt)
	{
		if (strpos($this->socketInfo[$sockInt]->readQueue, "\n") !== false)
		{
			return true;
		}
		return false;
	}

	public function getQueueLine($sockInt)
	{
		$readQueue =& $this->socketInfo[$sockInt]->readQueue;

		if (!$this->hasLine($sockInt))
		{
			return false;
		}

		$crlf = "\r\n";
		$crlfLen = 2;

		$lineEnds = strpos($readQueue, $crlf);

		if ($lineEnds === false)
		{
			$crlf = "\n";
			$crlfLen = 1;
			$lineEnds = strpos($readQueue, $crlf);
		}

		$line = substr($readQueue, 0, $lineEnds);
		$readQueue = substr($readQueue, $lineEnds + $crlfLen);

		$this->readQueueSize -= ($lineEnds + $crlfLen);
		$this->socketInfo[$sockInt]->readLength -= ($lineEnds + $crlfLen);
		
		if ($readQueue == "")
		{
			unset($this->socketInfo[$sockInt]->readQueue);
			$this->socketInfo[$sockInt]->readQueue = "";
		}

		return $line;
	}



	/* Misc HTTP Functions */

	public static function generatePostQuery($query, $host, $path, $httpVersion = "1.0")
	{
		if ($query != "" && substr($query, 0, 1) != "?")
		{
			$query = "?" . $query;
		}

		if ($path == "")
		{
			$path = "/";
		}

		$postQuery = "POST " . $path . " HTTP/".$httpVersion."\r\n";
		$postQuery .= "Host: " . $host . "\r\n";
		$postQuery .= "Content-type: application/x-www-form-urlencoded\r\n";
		$postQuery .= "Content-length: " . strlen($query) . "\r\n\r\n";
		$postQuery .= $query;

		return $postQuery;
	}

	public static function generateGetQuery($query, $host, $path, $httpVersion = "1.0")
	{
		if ($path == "")
		{
			$path = "/";
		}

		if ($query != "" && substr($query, 0, 1) != "?")
		{
			$query = "?" . $query;
		}

		$getQuery = "GET " . $path . $query . " HTTP/".$httpVersion."\r\n";
		$getQuery .= "Host: " . $host . "\r\n";
		$getQuery .= "Connection: close\r\n";
		$getQuery .= "\r\n";

		return $getQuery;
	}

}

?>