]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Irc/extlib/phergie/Phergie/Driver/Streams.php
Merge in Phergie fixes and remove some whitespace
[quix0rs-gnu-social.git] / plugins / Irc / extlib / phergie / Phergie / Driver / Streams.php
1 <?php
2 /**
3  * Phergie
4  *
5  * PHP version 5
6  *
7  * LICENSE
8  *
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
13  *
14  * @category  Phergie
15  * @package   Phergie
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
20  */
21
22 /**
23  * Driver that uses the sockets wrapper of the streams extension for
24  * communicating with the server and handles formatting and parsing of
25  * events using PHP.
26  *
27  * @category Phergie
28  * @package  Phergie
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
32  */
33 class Phergie_Driver_Streams extends Phergie_Driver_Abstract
34 {
35     /**
36      * Socket handlers
37      *
38      * @var array
39      */
40     protected $sockets = array();
41
42     /**
43      * Reference to the currently active socket handler
44      *
45      * @var resource
46      */
47     protected $socket;
48
49     /**
50      * Amount of time in seconds to wait to receive an event each time the
51      * socket is polled
52      *
53      * @var float
54      */
55     protected $timeout = 0.1;
56
57     /**
58      * Handles construction of command strings and their transmission to the
59      * server.
60      *
61      * @param string       $command Command to send
62      * @param string|array $args    Optional string or array of sequential
63      *        arguments
64      *
65      * @return string Command string that was sent
66      * @throws Phergie_Driver_Exception
67      */
68     protected function send($command, $args = '')
69     {
70         $connection = $this->getConnection();
71         $encoding = $connection->getEncoding();
72
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
78             );
79         }
80
81         // Add the command
82         $buffer = strtoupper($command);
83
84         // Add arguments
85         if (!empty($args)) {
86
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);
92             } else {
93                 $args = ':' . $args;
94             }
95
96             $buffer .= ' ' . $args;
97         }
98
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);
104         while (true) {
105             $written += (int) fwrite($this->socket, $temp);
106             if ($written < $length) {
107                 $temp = substr($temp, $written);
108                 $attempts++;
109                 if ($attempts == 3) {
110                     throw new Phergie_Driver_Exception(
111                         'Unable to write to socket',
112                         Phergie_Driver_Exception::ERR_CONNECTION_WRITE_FAILED
113                     );
114                 }
115             } else {
116                 break;
117             }
118         }
119
120         // Return the command string that was transmitted
121         return $buffer;
122     }
123
124     /**
125      * Overrides the parent class to set the currently active socket handler
126      * when the active connection is changed.
127      *
128      * @param Phergie_Connection $connection Active connection
129      *
130      * @return Phergie_Driver_Streams Provides a fluent interface
131      */
132     public function setConnection(Phergie_Connection $connection)
133     {
134         // Set the active socket handler
135         $hostmask = (string) $connection->getHostmask();
136         if (!empty($this->sockets[$hostmask])) {
137             $this->socket = $this->sockets[$hostmask];
138         }
139
140         // Set the active connection
141         return parent::setConnection($connection);
142     }
143
144     /**
145      * Returns a list of hostmasks corresponding to sockets with data to read.
146      *
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)
149      *
150      * @return array List of hostmasks or an empty array if none were found
151      *         to have data to read
152      */
153     public function getActiveReadSockets($sec = 0, $usec = 200000)
154     {
155         $read = $this->sockets;
156         $write = null;
157         $error = null;
158         $active = array();
159
160         if (count($this->sockets) > 0) {
161             $number = stream_select($read, $write, $error, $sec, $usec);
162             if ($number > 0) {
163                 foreach ($read as $item) {
164                     $active[] = array_search($item, $this->sockets);
165                 }
166             }
167         }
168
169         return $active;
170     }
171
172     /**
173      * Sets the amount of time to wait for a new event each time the socket
174      * is polled.
175      *
176      * @param float $timeout Amount of time in seconds
177      *
178      * @return Phergie_Driver_Streams Provides a fluent interface
179      */
180     public function setTimeout($timeout)
181     {
182         $timeout = (float) $timeout;
183         if ($timeout) {
184             $this->timeout = $timeout;
185         }
186         return $this;
187     }
188
189     /**
190      * Returns the amount of time to wait for a new event each time the
191      * socket is polled.
192      *
193      * @return float Amount of time in seconds
194      */
195     public function getTimeout()
196     {
197         return $this->timeout;
198     }
199
200     /**
201      * Supporting method to parse event argument strings where the last
202      * argument may contain a colon.
203      *
204      * @param string $args  Argument string to parse
205      * @param int    $count Optional maximum number of arguments
206      *
207      * @return array Array of argument values
208      */
209     protected function parseArguments($args, $count = -1)
210     {
211         return preg_split('/ :?/S', $args, $count);
212     }
213
214     /**
215      * Listens for an event on the current connection.
216      *
217      * @return Phergie_Event_Interface|null Event instance if an event was
218      *         received, NULL otherwise
219      */
220     public function getEvent()
221     {
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
227             );
228         }
229
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
236             );
237         }
238
239         // If no new event was found, return NULL
240         if (empty($buffer)) {
241             return null;
242         }
243
244         // Strip the trailing newline from the buffer
245         $buffer = rtrim($buffer);
246
247         // If the event is from the server...
248         if (substr($buffer, 0, 1) != ':') {
249
250             // Parse the command and arguments
251             list($cmd, $args) = array_pad(explode(' ', $buffer, 2), 2, null);
252
253         } else {
254             // If the event could be from the server or a user...
255
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);
261             } else {
262                 $hostmask = new Phergie_Hostmask(null, null, $prefix);
263             }
264         }
265
266         // Parse the event arguments depending on the event type
267         $cmd = strtolower($cmd);
268         switch ($cmd) {
269         case 'names':
270         case 'nick':
271         case 'quit':
272         case 'ping':
273         case 'join':
274         case 'error':
275             $args = array(ltrim($args, ':'));
276             break;
277
278         case 'privmsg':
279         case 'notice':
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);
287                 switch ($cmd) {
288                 case 'version':
289                 case 'time':
290                 case 'finger':
291                     if ($reply) {
292                         $args = $ctcp;
293                     }
294                     break;
295                 case 'ping':
296                     if ($reply) {
297                         $cmd .= 'Response';
298                     } else {
299                         $cmd = 'ctcpPing';
300                     }
301                     break;
302                 case 'action':
303                     $args = array($source, $args);
304                     break;
305
306                 default:
307                     $cmd = 'ctcp';
308                     if ($reply) {
309                         $cmd .= 'Response';
310                     }
311                     $args = array($source, $args);
312                     break;
313                 }
314             }
315             break;
316
317         case 'oper':
318         case 'topic':
319         case 'mode':
320             $args = $this->parseArguments($args);
321             break;
322
323         case 'part':
324         case 'kill':
325         case 'invite':
326             $args = $this->parseArguments($args, 2);
327             break;
328
329         case 'kick':
330             $args = $this->parseArguments($args, 3);
331             break;
332
333         // Remove the target from responses
334         default:
335             $args = substr($args, strpos($args, ' ') + 1);
336             break;
337         }
338
339         // Create, populate, and return an event object
340         if (ctype_digit($cmd)) {
341             $event = new Phergie_Event_Response;
342             $event
343                 ->setCode($cmd)
344                 ->setDescription($args);
345         } else {
346             $event = new Phergie_Event_Request;
347             $event
348                 ->setType($cmd)
349                 ->setArguments($args);
350             if (isset($hostmask)) {
351                 $event->setHostmask($hostmask);
352             }
353         }
354         $event->setRawData($buffer);
355         return $event;
356     }
357
358     /**
359      * Initiates a connection with the server.
360      *
361      * @return void
362      */
363     public function doConnect()
364     {
365         // Listen for input indefinitely
366         set_time_limit(0);
367
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();
377
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
385             );
386         }
387
388         $seconds = (int) $this->timeout;
389         $microseconds = ($this->timeout - $seconds) * 1000000;
390         stream_set_timeout($this->socket, $seconds, $microseconds);
391
392         // Send the password if one is specified
393         if (!empty($password)) {
394             $this->send('PASS', $password);
395         }
396
397         // Send user information
398         $this->send(
399             'USER',
400             array(
401                 $username,
402                 $hostname,
403                 $hostname,
404                 $realname
405             )
406         );
407
408         $this->send('NICK', $nick);
409
410         // Add the socket handler to the internal array for socket handlers
411         $this->sockets[(string) $connection->getHostmask()] = $this->socket;
412     }
413
414     /**
415      * Terminates the connection with the server.
416      *
417      * @param string $reason Reason for connection termination (optional)
418      *
419      * @return void
420      */
421     public function doQuit($reason = null)
422     {
423         // Send a QUIT command to the server
424         $this->send('QUIT', $reason);
425
426         // Terminate the socket connection
427         fclose($this->socket);
428
429         // Remove the socket from the internal socket list
430         unset($this->sockets[(string) $this->getConnection()->getHostmask()]);
431     }
432
433     /**
434      * Joins a channel.
435      *
436      * @param string $channels Comma-delimited list of channels to join
437      * @param string $keys     Optional comma-delimited list of channel keys
438      *
439      * @return void
440      */
441     public function doJoin($channels, $keys = null)
442     {
443         $args = array($channels);
444
445         if (!empty($keys)) {
446             $args[] = $keys;
447         }
448
449         $this->send('JOIN', $args);
450     }
451
452     /**
453      * Leaves a channel.
454      *
455      * @param string $channels Comma-delimited list of channels to leave
456      *
457      * @return void
458      */
459     public function doPart($channels)
460     {
461         $this->send('PART', $channels);
462     }
463
464     /**
465      * Invites a user to an invite-only channel.
466      *
467      * @param string $nick    Nick of the user to invite
468      * @param string $channel Name of the channel
469      *
470      * @return void
471      */
472     public function doInvite($nick, $channel)
473     {
474         $this->send('INVITE', array($nick, $channel));
475     }
476
477     /**
478      * Obtains a list of nicks of usrs in currently joined channels.
479      *
480      * @param string $channels Comma-delimited list of one or more channels
481      *
482      * @return void
483      */
484     public function doNames($channels)
485     {
486         $this->send('NAMES', $channels);
487     }
488
489     /**
490      * Obtains a list of channel names and topics.
491      *
492      * @param string $channels Comma-delimited list of one or more channels
493      *                         to which the response should be restricted
494      *                         (optional)
495      *
496      * @return void
497      */
498     public function doList($channels = null)
499     {
500         $this->send('LIST', $channels);
501     }
502
503     /**
504      * Retrieves or changes a channel topic.
505      *
506      * @param string $channel Name of the channel
507      * @param string $topic   New topic to assign (optional)
508      *
509      * @return void
510      */
511     public function doTopic($channel, $topic = null)
512     {
513         $args = array($channel);
514
515         if (!empty($topic)) {
516             $args[] = $topic;
517         }
518
519         $this->send('TOPIC', $args);
520     }
521
522     /**
523      * Retrieves or changes a channel or user mode.
524      *
525      * @param string $target Channel name or user nick
526      * @param string $mode   New mode to assign (optional)
527      *
528      * @return void
529      */
530     public function doMode($target, $mode = null)
531     {
532         $args = array($target);
533
534         if (!empty($mode)) {
535             $args[] = $mode;
536         }
537
538         $this->send('MODE', $args);
539     }
540
541     /**
542      * Changes the client nick.
543      *
544      * @param string $nick New nick to assign
545      *
546      * @return void
547      */
548     public function doNick($nick)
549     {
550         $this->send('NICK', $nick);
551     }
552
553     /**
554      * Retrieves information about a nick.
555      *
556      * @param string $nick Nick
557      *
558      * @return void
559      */
560     public function doWhois($nick)
561     {
562         $this->send('WHOIS', $nick);
563     }
564
565     /**
566      * Sends a message to a nick or channel.
567      *
568      * @param string $target Channel name or user nick
569      * @param string $text   Text of the message to send
570      *
571      * @return void
572      */
573     public function doPrivmsg($target, $text)
574     {
575         $this->send('PRIVMSG', array($target, $text));
576     }
577
578     /**
579      * Sends a notice to a nick or channel.
580      *
581      * @param string $target Channel name or user nick
582      * @param string $text   Text of the notice to send
583      *
584      * @return void
585      */
586     public function doNotice($target, $text)
587     {
588         $this->send('NOTICE', array($target, $text));
589     }
590
591     /**
592      * Kicks a user from a channel.
593      *
594      * @param string $nick    Nick of the user
595      * @param string $channel Channel name
596      * @param string $reason  Reason for the kick (optional)
597      *
598      * @return void
599      */
600     public function doKick($nick, $channel, $reason = null)
601     {
602         $args = array($nick, $channel);
603
604         if (!empty($reason)) {
605             $args[] = $response;
606         }
607
608         $this->send('KICK', $args);
609     }
610
611     /**
612      * Responds to a server test of client responsiveness.
613      *
614      * @param string $daemon Daemon from which the original request originates
615      *
616      * @return void
617      */
618     public function doPong($daemon)
619     {
620         $this->send('PONG', $daemon);
621     }
622
623     /**
624      * Sends a CTCP ACTION (/me) command to a nick or channel.
625      *
626      * @param string $target Channel name or user nick
627      * @param string $text   Text of the action to perform
628      *
629      * @return void
630      */
631     public function doAction($target, $text)
632     {
633         $buffer = rtrim('ACTION ' . $text);
634
635         $this->doPrivmsg($target, chr(1) . $buffer . chr(1));
636     }
637
638     /**
639      * Sends a CTCP response to a user.
640      *
641      * @param string       $nick    User nick
642      * @param string       $command Command to send
643      * @param string|array $args    String or array of sequential arguments
644      *        (optional)
645      *
646      * @return void
647      */
648     protected function doCtcp($nick, $command, $args = null)
649     {
650         if (is_array($args)) {
651             $args = implode(' ', $args);
652         }
653
654         $buffer = rtrim(strtoupper($command) . ' ' . $args);
655
656         $this->doNotice($nick, chr(1) . $buffer . chr(1));
657     }
658
659     /**
660      * Sends a CTCP PING request or response (they are identical) to a user.
661      *
662      * @param string $nick User nick
663      * @param string $hash Hash to use in the handshake
664      *
665      * @return void
666      */
667     public function doPing($nick, $hash)
668     {
669         $this->doCtcp($nick, 'PING', $hash);
670     }
671
672     /**
673      * Sends a CTCP VERSION request or response to a user.
674      *
675      * @param string $nick    User nick
676      * @param string $version Version string to send for a response
677      *
678      * @return void
679      */
680     public function doVersion($nick, $version = null)
681     {
682         if ($version) {
683             $this->doCtcp($nick, 'VERSION', $version);
684         } else {
685             $this->doCtcp($nick, 'VERSION');
686         }
687     }
688
689     /**
690      * Sends a CTCP TIME request to a user.
691      *
692      * @param string $nick User nick
693      * @param string $time Time string to send for a response
694      *
695      * @return void
696      */
697     public function doTime($nick, $time = null)
698     {
699         if ($time) {
700             $this->doCtcp($nick, 'TIME', $time);
701         } else {
702             $this->doCtcp($nick, 'TIME');
703         }
704     }
705
706     /**
707      * Sends a CTCP FINGER request to a user.
708      *
709      * @param string $nick   User nick
710      * @param string $finger Finger string to send for a response
711      *
712      * @return void
713      */
714     public function doFinger($nick, $finger = null)
715     {
716         if ($finger) {
717             $this->doCtcp($nick, 'FINGER', $finger);
718         } else {
719             $this->doCtcp($nick, 'FINGER');
720         }
721     }
722
723     /**
724      * Sends a raw command to the server.
725      *
726      * @param string $command Command string to send
727      *
728      * @return void
729      */
730     public function doRaw($command)
731     {
732         $this->send('RAW', $command);
733     }
734 }