123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730 |
- <?php
- /**
- * Phergie
- *
- * PHP version 5
- *
- * LICENSE
- *
- * This source file is subject to the new BSD license that is bundled
- * with this package in the file LICENSE.
- * It is also available through the world-wide-web at this URL:
- * http://phergie.org/license
- *
- * @category Phergie
- * @package Phergie
- * @author Phergie Development Team <team@phergie.org>
- * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
- * @license http://phergie.org/license New BSD License
- * @link http://pear.phergie.org/package/Phergie
- */
- /**
- * Driver that uses the sockets wrapper of the streams extension for
- * communicating with the server and handles formatting and parsing of
- * events using PHP.
- *
- * @category Phergie
- * @package Phergie
- * @author Phergie Development Team <team@phergie.org>
- * @license http://phergie.org/license New BSD License
- * @link http://pear.phergie.org/package/Phergie
- */
- class Phergie_Driver_Streams extends Phergie_Driver_Abstract
- {
- /**
- * Socket handlers
- *
- * @var array
- */
- protected $sockets = array();
- /**
- * Reference to the currently active socket handler
- *
- * @var resource
- */
- protected $socket;
- /**
- * Amount of time in seconds to wait to receive an event each time the
- * socket is polled
- *
- * @var float
- */
- protected $timeout = 0.1;
- /**
- * Handles construction of command strings and their transmission to the
- * server.
- *
- * @param string $command Command to send
- * @param string|array $args Optional string or array of sequential
- * arguments
- *
- * @return string Command string that was sent
- * @throws Phergie_Driver_Exception
- */
- protected function send($command, $args = '')
- {
- $connection = $this->getConnection();
- $encoding = $connection->getEncoding();
- // Require an open socket connection to continue
- if (empty($this->socket)) {
- throw new Phergie_Driver_Exception(
- 'doConnect() must be called first',
- Phergie_Driver_Exception::ERR_NO_INITIATED_CONNECTION
- );
- }
- // Add the command
- $buffer = strtoupper($command);
- // Add arguments
- if (!empty($args)) {
- // Apply formatting if arguments are passed in as an array
- if (is_array($args)) {
- $end = count($args) - 1;
- $args[$end] = ':' . $args[$end];
- $args = implode(' ', $args);
- } else {
- $args = ':' . $args;
- }
- $buffer .= ' ' . $args;
- }
- // Transmit the command over the socket connection
- $attempts = $written = 0;
- $temp = $buffer . "\r\n";
- $is_multibyte = !substr($encoding, 0, 8) === 'ISO-8859' && $encoding !== 'ASCII' && $encoding !== 'CP1252';
- $length = ($is_multibyte) ? mb_strlen($buffer, '8bit') : strlen($buffer);
- while (true) {
- $written += (int) fwrite($this->socket, $temp);
- if ($written < $length) {
- $temp = substr($temp, $written);
- $attempts++;
- if ($attempts == 3) {
- throw new Phergie_Driver_Exception(
- 'Unable to write to socket',
- Phergie_Driver_Exception::ERR_CONNECTION_WRITE_FAILED
- );
- }
- } else {
- break;
- }
- }
- // Return the command string that was transmitted
- return $buffer;
- }
- /**
- * Overrides the parent class to set the currently active socket handler
- * when the active connection is changed.
- *
- * @param Phergie_Connection $connection Active connection
- *
- * @return Phergie_Driver_Streams Provides a fluent interface
- */
- public function setConnection(Phergie_Connection $connection)
- {
- // Set the active socket handler
- $hostmask = (string) $connection->getHostmask();
- if (!empty($this->sockets[$hostmask])) {
- $this->socket = $this->sockets[$hostmask];
- }
- // Set the active connection
- return parent::setConnection($connection);
- }
- /**
- * Returns a list of hostmasks corresponding to sockets with data to read.
- *
- * @param int $sec Length of time to wait for new data (seconds)
- * @param int $usec Length of time to wait for new data (microseconds)
- *
- * @return array List of hostmasks or an empty array if none were found
- * to have data to read
- */
- public function getActiveReadSockets($sec = 0, $usec = 200000)
- {
- $read = $this->sockets;
- $write = null;
- $error = null;
- $active = array();
- if (count($this->sockets) > 0) {
- $number = stream_select($read, $write, $error, $sec, $usec);
- if ($number > 0) {
- foreach ($read as $item) {
- $active[] = array_search($item, $this->sockets);
- }
- }
- }
- return $active;
- }
- /**
- * Sets the amount of time to wait for a new event each time the socket
- * is polled.
- *
- * @param float $timeout Amount of time in seconds
- *
- * @return Phergie_Driver_Streams Provides a fluent interface
- */
- public function setTimeout($timeout)
- {
- $timeout = (float) $timeout;
- if ($timeout) {
- $this->timeout = $timeout;
- }
- return $this;
- }
- /**
- * Returns the amount of time to wait for a new event each time the
- * socket is polled.
- *
- * @return float Amount of time in seconds
- */
- public function getTimeout()
- {
- return $this->timeout;
- }
- /**
- * Supporting method to parse event argument strings where the last
- * argument may contain a colon.
- *
- * @param string $args Argument string to parse
- * @param int $count Optional maximum number of arguments
- *
- * @return array Array of argument values
- */
- protected function parseArguments($args, $count = -1)
- {
- return preg_split('/ :?/S', $args, $count);
- }
- /**
- * Listens for an event on the current connection.
- *
- * @return Phergie_Event_Interface|null Event instance if an event was
- * received, NULL otherwise
- */
- public function getEvent()
- {
- // Check the socket is still active
- if (feof($this->socket)) {
- throw new Phergie_Driver_Exception(
- 'EOF detected on socket',
- Phergie_Driver_Exception::ERR_CONNECTION_READ_FAILED
- );
- }
- // Check for a new event on the current connection
- $buffer = fgets($this->socket, 512);
- // If no new event was found, return NULL
- if (empty($buffer)) {
- return null;
- }
- // Strip the trailing newline from the buffer
- $buffer = rtrim($buffer);
- // If the event is from the server...
- if (substr($buffer, 0, 1) != ':') {
- // Parse the command and arguments
- list($cmd, $args) = array_pad(explode(' ', $buffer, 2), 2, null);
- $hostmask = new Phergie_Hostmask(null, null, $this->connection->getHost());
- } else {
- // If the event could be from the server or a user...
- // Parse the server hostname or user hostmask, command, and arguments
- list($prefix, $cmd, $args)
- = array_pad(explode(' ', ltrim($buffer, ':'), 3), 3, null);
- if (strpos($prefix, '@') !== false) {
- $hostmask = Phergie_Hostmask::fromString($prefix);
- } else {
- $hostmask = new Phergie_Hostmask(null, null, $prefix);
- }
- }
- // Parse the event arguments depending on the event type
- $cmd = strtolower($cmd);
- switch ($cmd) {
- case 'names':
- case 'nick':
- case 'quit':
- case 'ping':
- case 'join':
- case 'error':
- $args = array(ltrim($args, ':'));
- break;
- case 'privmsg':
- case 'notice':
- $args = $this->parseArguments($args, 2);
- list($source, $ctcp) = $args;
- if (substr($ctcp, 0, 1) === "\001" && substr($ctcp, -1) === "\001") {
- $ctcp = substr($ctcp, 1, -1);
- $reply = ($cmd == 'notice');
- list($cmd, $args) = array_pad(explode(' ', $ctcp, 2), 2, null);
- $cmd = strtolower($cmd);
- switch ($cmd) {
- case 'version':
- case 'time':
- case 'finger':
- if ($reply) {
- $args = $ctcp;
- }
- break;
- case 'ping':
- if ($reply) {
- $cmd .= 'Response';
- } else {
- $cmd = 'ctcpPing';
- }
- break;
- case 'action':
- $args = array($source, $args);
- break;
- default:
- $cmd = 'ctcp';
- if ($reply) {
- $cmd .= 'Response';
- }
- $args = array($source, $args);
- break;
- }
- }
- break;
- case 'oper':
- case 'topic':
- case 'mode':
- $args = $this->parseArguments($args);
- break;
- case 'part':
- case 'kill':
- case 'invite':
- $args = $this->parseArguments($args, 2);
- break;
- case 'kick':
- $args = $this->parseArguments($args, 3);
- break;
- // Remove the target from responses
- default:
- $args = substr($args, strpos($args, ' ') + 1);
- break;
- }
- // Create, populate, and return an event object
- if (ctype_digit($cmd)) {
- $event = new Phergie_Event_Response;
- $event
- ->setCode($cmd)
- ->setDescription($args);
- } else {
- $event = new Phergie_Event_Request;
- $event
- ->setType($cmd)
- ->setArguments($args);
- if (isset($hostmask)) {
- $event->setHostmask($hostmask);
- }
- }
- $event->setRawData($buffer);
- return $event;
- }
- /**
- * Initiates a connection with the server.
- *
- * @return void
- */
- public function doConnect()
- {
- // Listen for input indefinitely
- set_time_limit(0);
- // Get connection information
- $connection = $this->getConnection();
- $hostname = $connection->getHost();
- $port = $connection->getPort();
- $password = $connection->getPassword();
- $username = $connection->getUsername();
- $nick = $connection->getNick();
- $realname = $connection->getRealname();
- $transport = $connection->getTransport();
- // Establish and configure the socket connection
- $remote = $transport . '://' . $hostname . ':' . $port;
- $this->socket = @stream_socket_client($remote, $errno, $errstr);
- if (!$this->socket) {
- throw new Phergie_Driver_Exception(
- 'Unable to connect: socket error ' . $errno . ' ' . $errstr,
- Phergie_Driver_Exception::ERR_CONNECTION_ATTEMPT_FAILED
- );
- }
- $seconds = (int) $this->timeout;
- $microseconds = ($this->timeout - $seconds) * 1000000;
- stream_set_timeout($this->socket, $seconds, $microseconds);
- // Send the password if one is specified
- if (!empty($password)) {
- $this->send('PASS', $password);
- }
- // Send user information
- $this->send(
- 'USER',
- array(
- $username,
- $hostname,
- $hostname,
- $realname
- )
- );
- $this->send('NICK', $nick);
- // Add the socket handler to the internal array for socket handlers
- $this->sockets[(string) $connection->getHostmask()] = $this->socket;
- }
- /**
- * Terminates the connection with the server.
- *
- * @param string $reason Reason for connection termination (optional)
- *
- * @return void
- */
- public function doQuit($reason = null)
- {
- // Send a QUIT command to the server
- $this->send('QUIT', $reason);
- // Terminate the socket connection
- fclose($this->socket);
- // Remove the socket from the internal socket list
- unset($this->sockets[(string) $this->getConnection()->getHostmask()]);
- }
- /**
- * Joins a channel.
- *
- * @param string $channels Comma-delimited list of channels to join
- * @param string $keys Optional comma-delimited list of channel keys
- *
- * @return void
- */
- public function doJoin($channels, $keys = null)
- {
- $args = array($channels);
- if (!empty($keys)) {
- $args[] = $keys;
- }
- $this->send('JOIN', $args);
- }
- /**
- * Leaves a channel.
- *
- * @param string $channels Comma-delimited list of channels to leave
- *
- * @return void
- */
- public function doPart($channels)
- {
- $this->send('PART', $channels);
- }
- /**
- * Invites a user to an invite-only channel.
- *
- * @param string $nick Nick of the user to invite
- * @param string $channel Name of the channel
- *
- * @return void
- */
- public function doInvite($nick, $channel)
- {
- $this->send('INVITE', array($nick, $channel));
- }
- /**
- * Obtains a list of nicks of usrs in currently joined channels.
- *
- * @param string $channels Comma-delimited list of one or more channels
- *
- * @return void
- */
- public function doNames($channels)
- {
- $this->send('NAMES', $channels);
- }
- /**
- * Obtains a list of channel names and topics.
- *
- * @param string $channels Comma-delimited list of one or more channels
- * to which the response should be restricted
- * (optional)
- *
- * @return void
- */
- public function doList($channels = null)
- {
- $this->send('LIST', $channels);
- }
- /**
- * Retrieves or changes a channel topic.
- *
- * @param string $channel Name of the channel
- * @param string $topic New topic to assign (optional)
- *
- * @return void
- */
- public function doTopic($channel, $topic = null)
- {
- $args = array($channel);
- if (!empty($topic)) {
- $args[] = $topic;
- }
- $this->send('TOPIC', $args);
- }
- /**
- * Retrieves or changes a channel or user mode.
- *
- * @param string $target Channel name or user nick
- * @param string $mode New mode to assign (optional)
- *
- * @return void
- */
- public function doMode($target, $mode = null)
- {
- $args = array($target);
- if (!empty($mode)) {
- $args[] = $mode;
- }
- $this->send('MODE', $args);
- }
- /**
- * Changes the client nick.
- *
- * @param string $nick New nick to assign
- *
- * @return void
- */
- public function doNick($nick)
- {
- $this->send('NICK', $nick);
- }
- /**
- * Retrieves information about a nick.
- *
- * @param string $nick Nick
- *
- * @return void
- */
- public function doWhois($nick)
- {
- $this->send('WHOIS', $nick);
- }
- /**
- * Sends a message to a nick or channel.
- *
- * @param string $target Channel name or user nick
- * @param string $text Text of the message to send
- *
- * @return void
- */
- public function doPrivmsg($target, $text)
- {
- $this->send('PRIVMSG', array($target, $text));
- }
- /**
- * Sends a notice to a nick or channel.
- *
- * @param string $target Channel name or user nick
- * @param string $text Text of the notice to send
- *
- * @return void
- */
- public function doNotice($target, $text)
- {
- $this->send('NOTICE', array($target, $text));
- }
- /**
- * Kicks a user from a channel.
- *
- * @param string $nick Nick of the user
- * @param string $channel Channel name
- * @param string $reason Reason for the kick (optional)
- *
- * @return void
- */
- public function doKick($nick, $channel, $reason = null)
- {
- $args = array($nick, $channel);
- if (!empty($reason)) {
- $args[] = $response;
- }
- $this->send('KICK', $args);
- }
- /**
- * Responds to a server test of client responsiveness.
- *
- * @param string $daemon Daemon from which the original request originates
- *
- * @return void
- */
- public function doPong($daemon)
- {
- $this->send('PONG', $daemon);
- }
- /**
- * Sends a CTCP ACTION (/me) command to a nick or channel.
- *
- * @param string $target Channel name or user nick
- * @param string $text Text of the action to perform
- *
- * @return void
- */
- public function doAction($target, $text)
- {
- $buffer = rtrim('ACTION ' . $text);
- $this->doPrivmsg($target, chr(1) . $buffer . chr(1));
- }
- /**
- * Sends a CTCP response to a user.
- *
- * @param string $nick User nick
- * @param string $command Command to send
- * @param string|array $args String or array of sequential arguments
- * (optional)
- *
- * @return void
- */
- protected function doCtcp($nick, $command, $args = null)
- {
- if (is_array($args)) {
- $args = implode(' ', $args);
- }
- $buffer = rtrim(strtoupper($command) . ' ' . $args);
- $this->doNotice($nick, chr(1) . $buffer . chr(1));
- }
- /**
- * Sends a CTCP PING request or response (they are identical) to a user.
- *
- * @param string $nick User nick
- * @param string $hash Hash to use in the handshake
- *
- * @return void
- */
- public function doPing($nick, $hash)
- {
- $this->doCtcp($nick, 'PING', $hash);
- }
- /**
- * Sends a CTCP VERSION request or response to a user.
- *
- * @param string $nick User nick
- * @param string $version Version string to send for a response
- *
- * @return void
- */
- public function doVersion($nick, $version = null)
- {
- if ($version) {
- $this->doCtcp($nick, 'VERSION', $version);
- } else {
- $this->doCtcp($nick, 'VERSION');
- }
- }
- /**
- * Sends a CTCP TIME request to a user.
- *
- * @param string $nick User nick
- * @param string $time Time string to send for a response
- *
- * @return void
- */
- public function doTime($nick, $time = null)
- {
- if ($time) {
- $this->doCtcp($nick, 'TIME', $time);
- } else {
- $this->doCtcp($nick, 'TIME');
- }
- }
- /**
- * Sends a CTCP FINGER request to a user.
- *
- * @param string $nick User nick
- * @param string $finger Finger string to send for a response
- *
- * @return void
- */
- public function doFinger($nick, $finger = null)
- {
- if ($finger) {
- $this->doCtcp($nick, 'FINGER', $finger);
- } else {
- $this->doCtcp($nick, 'FINGER');
- }
- }
- /**
- * Sends a raw command to the server.
- *
- * @param string $command Command string to send
- *
- * @return void
- */
- public function doRaw($command)
- {
- $this->send('RAW', $command);
- }
- }
|