9 * This source file is subject to the new BSD license that is bundled
10 * with this package in the file LICENSE.
11 * It is also available through the world-wide-web at this URL:
12 * http://phergie.org/license
16 * @author Phergie Development Team <team@phergie.org>
17 * @copyright 2008-2010 Phergie Development Team (http://phergie.org)
18 * @license http://phergie.org/license New BSD License
19 * @link http://pear.phergie.org/package/Phergie
23 * Driver that uses the sockets wrapper of the streams extension for
24 * communicating with the server and handles formatting and parsing of
29 * @author Phergie Development Team <team@phergie.org>
30 * @license http://phergie.org/license New BSD License
31 * @link http://pear.phergie.org/package/Phergie
33 class Phergie_Driver_Streams extends Phergie_Driver_Abstract
40 protected $sockets = array();
43 * Reference to the currently active socket handler
50 * Amount of time in seconds to wait to receive an event each time the
55 protected $timeout = 0.1;
58 * Handles construction of command strings and their transmission to the
61 * @param string $command Command to send
62 * @param string|array $args Optional string or array of sequential
65 * @return string Command string that was sent
66 * @throws Phergie_Driver_Exception
68 protected function send($command, $args = '')
70 $connection = $this->getConnection();
71 $encoding = $connection->getEncoding();
73 // Require an open socket connection to continue
74 if (empty($this->socket)) {
75 throw new Phergie_Driver_Exception(
76 'doConnect() must be called first',
77 Phergie_Driver_Exception::ERR_NO_INITIATED_CONNECTION
82 $buffer = strtoupper($command);
87 // Apply formatting if arguments are passed in as an array
88 if (is_array($args)) {
89 $end = count($args) - 1;
90 $args[$end] = ':' . $args[$end];
91 $args = implode(' ', $args);
96 $buffer .= ' ' . $args;
99 // Transmit the command over the socket connection
100 $attempts = $written = 0;
101 $temp = $buffer . "\r\n";
102 $is_multibyte = !substr($encoding, 0, 8) === 'ISO-8859' && $encoding !== 'ASCII' && $encoding !== 'CP1252';
103 $length = ($is_multibyte) ? mb_strlen($buffer, '8bit') : strlen($buffer);
105 $written += (int) fwrite($this->socket, $temp);
106 if ($written < $length) {
107 $temp = substr($temp, $written);
109 if ($attempts == 3) {
110 throw new Phergie_Driver_Exception(
111 'Unable to write to socket',
112 Phergie_Driver_Exception::ERR_CONNECTION_WRITE_FAILED
120 // Return the command string that was transmitted
125 * Overrides the parent class to set the currently active socket handler
126 * when the active connection is changed.
128 * @param Phergie_Connection $connection Active connection
130 * @return Phergie_Driver_Streams Provides a fluent interface
132 public function setConnection(Phergie_Connection $connection)
134 // Set the active socket handler
135 $hostmask = (string) $connection->getHostmask();
136 if (!empty($this->sockets[$hostmask])) {
137 $this->socket = $this->sockets[$hostmask];
140 // Set the active connection
141 return parent::setConnection($connection);
145 * Returns a list of hostmasks corresponding to sockets with data to read.
147 * @param int $sec Length of time to wait for new data (seconds)
148 * @param int $usec Length of time to wait for new data (microseconds)
150 * @return array List of hostmasks or an empty array if none were found
151 * to have data to read
153 public function getActiveReadSockets($sec = 0, $usec = 200000)
155 $read = $this->sockets;
160 if (count($this->sockets) > 0) {
161 $number = stream_select($read, $write, $error, $sec, $usec);
163 foreach ($read as $item) {
164 $active[] = array_search($item, $this->sockets);
173 * Sets the amount of time to wait for a new event each time the socket
176 * @param float $timeout Amount of time in seconds
178 * @return Phergie_Driver_Streams Provides a fluent interface
180 public function setTimeout($timeout)
182 $timeout = (float) $timeout;
184 $this->timeout = $timeout;
190 * Returns the amount of time to wait for a new event each time the
193 * @return float Amount of time in seconds
195 public function getTimeout()
197 return $this->timeout;
201 * Supporting method to parse event argument strings where the last
202 * argument may contain a colon.
204 * @param string $args Argument string to parse
205 * @param int $count Optional maximum number of arguments
207 * @return array Array of argument values
209 protected function parseArguments($args, $count = -1)
211 return preg_split('/ :?/S', $args, $count);
215 * Listens for an event on the current connection.
217 * @return Phergie_Event_Interface|null Event instance if an event was
218 * received, NULL otherwise
220 public function getEvent()
222 // Check the socket is still active
223 if (feof($this->socket)) {
224 throw new Phergie_Driver_Exception(
225 'EOF detected on socket',
226 Phergie_Driver_Exception::ERR_CONNECTION_READ_FAILED
230 // Check for a new event on the current connection
231 $buffer = fgets($this->socket, 512);
232 if ($buffer === false) {
233 throw new Phergie_Driver_Exception(
234 'Unable to read from socket',
235 Phergie_Driver_Exception::ERR_CONNECTION_READ_FAILED
239 // If no new event was found, return NULL
240 if (empty($buffer)) {
244 // Strip the trailing newline from the buffer
245 $buffer = rtrim($buffer);
247 // If the event is from the server...
248 if (substr($buffer, 0, 1) != ':') {
250 // Parse the command and arguments
251 list($cmd, $args) = array_pad(explode(' ', $buffer, 2), 2, null);
254 // If the event could be from the server or a user...
256 // Parse the server hostname or user hostmask, command, and arguments
257 list($prefix, $cmd, $args)
258 = array_pad(explode(' ', ltrim($buffer, ':'), 3), 3, null);
259 if (strpos($prefix, '@') !== false) {
260 $hostmask = Phergie_Hostmask::fromString($prefix);
262 $hostmask = new Phergie_Hostmask(null, null, $prefix);
266 // Parse the event arguments depending on the event type
267 $cmd = strtolower($cmd);
275 $args = array(ltrim($args, ':'));
280 $args = $this->parseArguments($args, 2);
281 list($source, $ctcp) = $args;
282 if (substr($ctcp, 0, 1) === "\001" && substr($ctcp, -1) === "\001") {
283 $ctcp = substr($ctcp, 1, -1);
284 $reply = ($cmd == 'notice');
285 list($cmd, $args) = array_pad(explode(' ', $ctcp, 2), 2, null);
286 $cmd = strtolower($cmd);
303 $args = array($source, $args);
311 $args = array($source, $args);
320 $args = $this->parseArguments($args);
326 $args = $this->parseArguments($args, 2);
330 $args = $this->parseArguments($args, 3);
333 // Remove the target from responses
335 $args = substr($args, strpos($args, ' ') + 1);
339 // Create, populate, and return an event object
340 if (ctype_digit($cmd)) {
341 $event = new Phergie_Event_Response;
344 ->setDescription($args);
346 $event = new Phergie_Event_Request;
349 ->setArguments($args);
350 if (isset($hostmask)) {
351 $event->setHostmask($hostmask);
354 $event->setRawData($buffer);
359 * Initiates a connection with the server.
363 public function doConnect()
365 // Listen for input indefinitely
368 // Get connection information
369 $connection = $this->getConnection();
370 $hostname = $connection->getHost();
371 $port = $connection->getPort();
372 $password = $connection->getPassword();
373 $username = $connection->getUsername();
374 $nick = $connection->getNick();
375 $realname = $connection->getRealname();
376 $transport = $connection->getTransport();
378 // Establish and configure the socket connection
379 $remote = $transport . '://' . $hostname . ':' . $port;
380 $this->socket = @stream_socket_client($remote, $errno, $errstr);
381 if (!$this->socket) {
382 throw new Phergie_Driver_Exception(
383 'Unable to connect: socket error ' . $errno . ' ' . $errstr,
384 Phergie_Driver_Exception::ERR_CONNECTION_ATTEMPT_FAILED
388 $seconds = (int) $this->timeout;
389 $microseconds = ($this->timeout - $seconds) * 1000000;
390 stream_set_timeout($this->socket, $seconds, $microseconds);
392 // Send the password if one is specified
393 if (!empty($password)) {
394 $this->send('PASS', $password);
397 // Send user information
408 $this->send('NICK', $nick);
410 // Add the socket handler to the internal array for socket handlers
411 $this->sockets[(string) $connection->getHostmask()] = $this->socket;
415 * Terminates the connection with the server.
417 * @param string $reason Reason for connection termination (optional)
421 public function doQuit($reason = null)
423 // Send a QUIT command to the server
424 $this->send('QUIT', $reason);
426 // Terminate the socket connection
427 fclose($this->socket);
429 // Remove the socket from the internal socket list
430 unset($this->sockets[(string) $this->getConnection()->getHostmask()]);
436 * @param string $channels Comma-delimited list of channels to join
437 * @param string $keys Optional comma-delimited list of channel keys
441 public function doJoin($channels, $keys = null)
443 $args = array($channels);
449 $this->send('JOIN', $args);
455 * @param string $channels Comma-delimited list of channels to leave
459 public function doPart($channels)
461 $this->send('PART', $channels);
465 * Invites a user to an invite-only channel.
467 * @param string $nick Nick of the user to invite
468 * @param string $channel Name of the channel
472 public function doInvite($nick, $channel)
474 $this->send('INVITE', array($nick, $channel));
478 * Obtains a list of nicks of usrs in currently joined channels.
480 * @param string $channels Comma-delimited list of one or more channels
484 public function doNames($channels)
486 $this->send('NAMES', $channels);
490 * Obtains a list of channel names and topics.
492 * @param string $channels Comma-delimited list of one or more channels
493 * to which the response should be restricted
498 public function doList($channels = null)
500 $this->send('LIST', $channels);
504 * Retrieves or changes a channel topic.
506 * @param string $channel Name of the channel
507 * @param string $topic New topic to assign (optional)
511 public function doTopic($channel, $topic = null)
513 $args = array($channel);
515 if (!empty($topic)) {
519 $this->send('TOPIC', $args);
523 * Retrieves or changes a channel or user mode.
525 * @param string $target Channel name or user nick
526 * @param string $mode New mode to assign (optional)
530 public function doMode($target, $mode = null)
532 $args = array($target);
538 $this->send('MODE', $args);
542 * Changes the client nick.
544 * @param string $nick New nick to assign
548 public function doNick($nick)
550 $this->send('NICK', $nick);
554 * Retrieves information about a nick.
556 * @param string $nick Nick
560 public function doWhois($nick)
562 $this->send('WHOIS', $nick);
566 * Sends a message to a nick or channel.
568 * @param string $target Channel name or user nick
569 * @param string $text Text of the message to send
573 public function doPrivmsg($target, $text)
575 $this->send('PRIVMSG', array($target, $text));
579 * Sends a notice to a nick or channel.
581 * @param string $target Channel name or user nick
582 * @param string $text Text of the notice to send
586 public function doNotice($target, $text)
588 $this->send('NOTICE', array($target, $text));
592 * Kicks a user from a channel.
594 * @param string $nick Nick of the user
595 * @param string $channel Channel name
596 * @param string $reason Reason for the kick (optional)
600 public function doKick($nick, $channel, $reason = null)
602 $args = array($nick, $channel);
604 if (!empty($reason)) {
608 $this->send('KICK', $args);
612 * Responds to a server test of client responsiveness.
614 * @param string $daemon Daemon from which the original request originates
618 public function doPong($daemon)
620 $this->send('PONG', $daemon);
624 * Sends a CTCP ACTION (/me) command to a nick or channel.
626 * @param string $target Channel name or user nick
627 * @param string $text Text of the action to perform
631 public function doAction($target, $text)
633 $buffer = rtrim('ACTION ' . $text);
635 $this->doPrivmsg($target, chr(1) . $buffer . chr(1));
639 * Sends a CTCP response to a user.
641 * @param string $nick User nick
642 * @param string $command Command to send
643 * @param string|array $args String or array of sequential arguments
648 protected function doCtcp($nick, $command, $args = null)
650 if (is_array($args)) {
651 $args = implode(' ', $args);
654 $buffer = rtrim(strtoupper($command) . ' ' . $args);
656 $this->doNotice($nick, chr(1) . $buffer . chr(1));
660 * Sends a CTCP PING request or response (they are identical) to a user.
662 * @param string $nick User nick
663 * @param string $hash Hash to use in the handshake
667 public function doPing($nick, $hash)
669 $this->doCtcp($nick, 'PING', $hash);
673 * Sends a CTCP VERSION request or response to a user.
675 * @param string $nick User nick
676 * @param string $version Version string to send for a response
680 public function doVersion($nick, $version = null)
683 $this->doCtcp($nick, 'VERSION', $version);
685 $this->doCtcp($nick, 'VERSION');
690 * Sends a CTCP TIME request to a user.
692 * @param string $nick User nick
693 * @param string $time Time string to send for a response
697 public function doTime($nick, $time = null)
700 $this->doCtcp($nick, 'TIME', $time);
702 $this->doCtcp($nick, 'TIME');
707 * Sends a CTCP FINGER request to a user.
709 * @param string $nick User nick
710 * @param string $finger Finger string to send for a response
714 public function doFinger($nick, $finger = null)
717 $this->doCtcp($nick, 'FINGER', $finger);
719 $this->doCtcp($nick, 'FINGER');
724 * Sends a raw command to the server.
726 * @param string $command Command string to send
730 public function doRaw($command)
732 $this->send('RAW', $command);