Streams.php 19 KB

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