PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/Phergie/Driver/Streams.php

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