PageRenderTime 52ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/Xmpp/Connection.php

https://github.com/donglinkang/Xmpp
PHP | 1121 lines | 891 code | 51 blank | 179 comment | 7 complexity | ed4f3a41e50b0eaeeb9aa3a5607bf64e MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Xmpp
  4. *
  5. * Xmpp is a implementation of the Xmpp protocol.
  6. *
  7. * PHP version 5
  8. *
  9. * LICENSE
  10. *
  11. * This source file is subject to the new BSD license that is bundled
  12. * with this package in the file LICENSE.
  13. * It is also available through the world-wide-web at this URL:
  14. * http://m.me.uk/xmpp/license/
  15. *
  16. * @category Xmpp
  17. * @package Xmpp
  18. * @author Alex Mace <a@m.me.uk>
  19. * @copyright 2010-2011 Alex Mace (http://m.me.uk)
  20. * @license http://m.me.uk/xmpp/license/ New BSD License
  21. * @link http://pear.m.me.uk/package/Xmpp
  22. */
  23. /**
  24. * Xmpp is an implementation of the Xmpp protocol. Note that creating the class
  25. * does not connect to the server specified in the constructor. You need to call
  26. * connect() to actually perform the connection.
  27. *
  28. * @category Xmpp
  29. * @package Xmpp
  30. * @author Alex Mace <a@m.me.uk>
  31. * @license http://m.me.uk/xmpp/license/ New BSD License
  32. * @link http://pear.m.me.uk/package/Xmpp
  33. * @todo Store what features are available when session is established
  34. * @todo Handle error conditions of authenticate, bind, connect and
  35. * establishSession
  36. * @todo Throw exceptions when attempting to perform actions that the server has
  37. * not reported that they support.
  38. * @todo ->wait() method should return a class that encapsulates what has come
  39. * from the server. e.g. Xmpp_Message Xmpp_Iq Xmpp_Presence
  40. */
  41. class Xmpp_Connection
  42. {
  43. const PRESENCE_AWAY = 'away';
  44. const PRESENCE_CHAT = 'chat';
  45. const PRESENCE_DND = 'dnd';
  46. const PRESENCE_XA = 'xa';
  47. /**
  48. * Holds the buffer of incoming tags
  49. *
  50. * @var array
  51. */
  52. private $_buffer = array();
  53. /**
  54. * Host name of the server to connect to
  55. *
  56. * @var string
  57. */
  58. private $_host = null;
  59. /**
  60. * List of items available on the server
  61. *
  62. * @var array
  63. */
  64. protected $items = null;
  65. /**
  66. * Holds an array of rooms that have been joined on this connection.
  67. *
  68. * @var array
  69. */
  70. protected $joinedRooms = array();
  71. private $_lastResponse = null;
  72. /**
  73. * Class that performs logging
  74. *
  75. * @var Zend_Log
  76. */
  77. private $_logger = null;
  78. private $_mechanisms = array();
  79. /**
  80. * Holds the password of the user we are going to connect with
  81. *
  82. * @var string
  83. */
  84. private $_password = null;
  85. /**
  86. * Holds the port of the server to connect to
  87. *
  88. * @var int
  89. */
  90. private $_port = null;
  91. /**
  92. * Holds the "realm" of the user name. Usually refers to the domain in the
  93. * user name.
  94. *
  95. * @var string
  96. */
  97. private $_realm = '';
  98. /**
  99. * Holds the resource for the connection. Will be something like a machine
  100. * name or a location to identify the connection.
  101. *
  102. * @var string
  103. */
  104. private $_resource = '';
  105. /**
  106. * Whether or not this connection to switch SSL when it is available.
  107. *
  108. * @var boolean
  109. */
  110. private $_ssl = true;
  111. /**
  112. * Holds the Stream object that performs the actual connection to the server
  113. *
  114. * @var Stream
  115. */
  116. private $_stream = null;
  117. /**
  118. * Holds the username used for authentication with the server
  119. *
  120. * @var string
  121. */
  122. private $_userName = null;
  123. /**
  124. * Class constructor
  125. *
  126. * @param string $userName Username to authenticate with
  127. * @param string $password Password to authenticate with
  128. * @param string $host Host name of the server to connect to
  129. * @param string $ssl Whether or not to connect over SSL if it is
  130. * available.
  131. * @param int $logLevel Level of logging to be performed
  132. * @param int $port Port to use for the connection
  133. * @param string $resource Identifier of the connection
  134. */
  135. public function __construct(
  136. $userName, $password, $host, $ssl = true, $logLevel = Zend_Log::EMERG,
  137. $port = 5222, $resource = 'NewXmpp'
  138. ) {
  139. // First set up logging
  140. $this->_host = $host;
  141. $this->_logger = $this->getLogger($logLevel);
  142. $this->_password = $password;
  143. $this->_port = $port;
  144. $this->_resource = $resource;
  145. $this->_ssl = $ssl;
  146. list($this->_userName, $this->_realm)
  147. = array_pad(explode('@', $userName), 2, null);
  148. }
  149. /**
  150. * Authenticate against server with the stored username and password.
  151. *
  152. * Note only DIGEST-MD5 authentication is supported.
  153. *
  154. * @return boolean
  155. */
  156. public function authenticate()
  157. {
  158. // Check that the server said that DIGEST-MD5 was available
  159. if ($this->mechanismAvailable('DIGEST-MD5')) {
  160. // Send message to the server that we want to authenticate
  161. $message = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
  162. . "mechanism='DIGEST-MD5'/>";
  163. $this->_logger->debug('Requesting Authentication: ' . $message);
  164. $this->_stream->send($message);
  165. // Wait for challenge to come back from the server
  166. $response = $this->waitForServer('challenge');
  167. $this->_logger->debug('Response: ' . $response->asXML());
  168. // Decode the response
  169. $decodedResponse = base64_decode((string) $response);
  170. $this->_logger->debug('Response (Decoded): ' . $decodedResponse);
  171. // Split up the parts of the challenge
  172. $challengeParts = explode(',', $decodedResponse);
  173. // Create an array to hold the challenge
  174. $challenge = array();
  175. // Process the parts and put them into the array for easy access
  176. foreach ($challengeParts as $part) {
  177. list($key, $value) = explode('=', trim($part), 2);
  178. $challenge[$key] = trim($value, '"');
  179. }
  180. // Ejabberd Doesn't appear to send the realm in the challenge, so
  181. // we need to default to what we think the realm is.
  182. if (!isset($challenge['realm'])) {
  183. $challenge['realm'] = $this->_realm;
  184. }
  185. $cnonce = uniqid();
  186. $a1 = pack(
  187. 'H32',
  188. md5(
  189. $this->_userName . ':' . $challenge['realm'] . ':' .
  190. $this->_password
  191. )
  192. )
  193. . ':' . $challenge['nonce'] . ':' . $cnonce;
  194. $a2 = 'AUTHENTICATE:xmpp/' . $challenge['realm'];
  195. $ha1 = md5($a1);
  196. $ha2 = md5($a2);
  197. $kd = $ha1 . ':' . $challenge['nonce'] . ':00000001:'
  198. . $cnonce . ':' . $challenge['qop'] . ':' . $ha2;
  199. $z = md5($kd);
  200. // Start constructing message to send with authentication details in
  201. // it.
  202. $message = 'username="' . $this->_userName . '",'
  203. . 'realm="' . $challenge['realm'] . '",'
  204. . 'nonce="' . $challenge['nonce'] . '",'
  205. . 'cnonce="' . $cnonce . '",nc="00000001",'
  206. . 'qop="' . $challenge['qop'] . '",'
  207. . 'digest-uri="xmpp/' . $challenge['realm'] . '",'
  208. . 'response="' . $z . '",'
  209. . 'charset="' . $challenge['charset'] . '"';
  210. $this->_logger->debug('Unencoded Response: ' . $message);
  211. $message = "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"
  212. . base64_encode($message) . '</response>';
  213. // Send the response
  214. $this->_logger->debug('Challenge Response: ' . $message);
  215. $this->_stream->send($message);
  216. // Should get another challenge back from the server. Some servers
  217. // don't bother though and just send a success back with the
  218. // rspauth encoded in it.
  219. $response = $this->waitForServer('*');
  220. $this->_logger->debug('Response: ' . $response->asXML());
  221. // If we have got a challenge, we need to send a response, blank
  222. // this time.
  223. if ($response->getName() == 'challenge') {
  224. $message = "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>";
  225. // Send the response
  226. $this->_logger->debug('Challenge Response: ' . $message);
  227. $this->_stream->send($message);
  228. // This time we should get a success message.
  229. $response = $this->waitForServer('success');
  230. $this->_logger->debug('Response: ' . $response->asXML());
  231. }
  232. // Now that we have been authenticated, a new stream needs to be
  233. // started.
  234. $this->startStream();
  235. // Server should now respond with start of stream and list of features
  236. $response = $this->waitForServer('stream:stream');
  237. $this->_logger->debug('Received: ' . $response);
  238. // If the server has not yet said what features it supports, wait
  239. // for that
  240. if (strpos($response->asXML(), 'stream:features') === false) {
  241. $response = $this->waitForServer('stream:features');
  242. $this->_logger->debug('Received: ' . $response);
  243. }
  244. }
  245. return true;
  246. }
  247. /**
  248. * Bind this connection to a particular resource (the last part of the JID)
  249. *
  250. * @return true
  251. */
  252. public function bind()
  253. {
  254. // Need to bind the resource with the server
  255. $message = "<iq type='set' id='bind_2'>"
  256. . "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>"
  257. . '<resource>' . $this->_resource . '</resource>'
  258. . '</bind></iq>';
  259. $this->_logger->debug('Bind request: ' . $message);
  260. $this->_stream->send($message);
  261. // Should get an iq response from the server confirming the jid
  262. $response = $this->waitForServer('*');
  263. $this->_logger->debug('Response: ' . $response->asXML());
  264. return true;
  265. }
  266. /**
  267. * Connects to the server and upgrades to TLS connection if possible
  268. *
  269. * @return void
  270. */
  271. public function connect()
  272. {
  273. // Figure out where we need to connect to
  274. $server = $this->getServer();
  275. try {
  276. // Get a connection to server
  277. $this->_stream = $this->getStream($server);
  278. $this->_logger->debug('Connection made');
  279. // Set the stream to blocking mode
  280. $this->_stream->setBlocking(true);
  281. $this->_logger->debug('Blocking enabled');
  282. // Attempt to send the stream start
  283. $this->startStream();
  284. $this->_logger->debug('Wait for response from server');
  285. // Now we will expect to get a stream tag back from the server. Not
  286. // sure if we're supposed to do anything with it, so we'll just drop
  287. // it for now. May contain the features the server supports.
  288. $response = $this->waitForServer('stream:stream');
  289. $this->_logger->debug('Received: ' . $response);
  290. // If the response from the server does contain a features tag, don't
  291. // bother querying server to get it.
  292. // TODO - Xpath would probably be more sensible for this, but for
  293. // now this'll work.
  294. if (strpos($response->asXml(), '<stream:features') === false) {
  295. // Server should now send back a features tag telling us what
  296. // features it supports. If it tells us to start tls then we will
  297. // need to change to a secure connection. It will also tell us what
  298. // authentication methods it supports.
  299. //
  300. // Note we check for a "features" tag rather than stream:features
  301. // because it is namespaced.
  302. $response = $this->waitForServer('features');
  303. $this->_logger->debug('Received: ' . $response);
  304. }
  305. // Set mechanisms based on that tag
  306. $this->setMechanisms($response);
  307. // If there was a starttls tag in there, and this connection has SSL
  308. // enabled, then we should tell the server that we will start up tls as
  309. // well.
  310. if (preg_match("/<starttls xmlns=('|\")urn:ietf:params:xml:ns:xmpp-tls('|\")>(<required\/>)?<\/starttls>/", $response->asXML()) != 0
  311. && $this->_ssl === true
  312. ) {
  313. $this->_logger->debug('Informing server we will start TLS');
  314. $message = "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>";
  315. $this->_stream->send($message);
  316. // Wait to get the proceed message back from the server
  317. $response = $this->waitForServer('proceed');
  318. $this->_logger->debug('Received: ' . $response->asXML());
  319. // Once we have the proceed signal from the server, we should turn
  320. // on TLS on the stream and send the opening stream tag again.
  321. $this->_stream->setTLS(true);
  322. $this->_logger->debug('Enabled TLS');
  323. // Now we need to start a new stream again.
  324. $this->startStream();
  325. // Server should now respond with start of stream and list of
  326. // features
  327. $response = $this->waitForServer('stream:stream');
  328. $this->_logger->debug('Received: ' . $response);
  329. // Set mechanisms based on that tag
  330. $this->setMechanisms($response);
  331. }
  332. } catch (Stream_Exception $e) {
  333. // A Stream Exception occured. Catch it and rethrow it as an Xmpp
  334. // Exception.
  335. throw new Xmpp_Exception('Failed to connect: ' . $e->getMessage());
  336. }
  337. return true;
  338. }
  339. /**
  340. * Disconnect from the server.
  341. *
  342. * @return boolean
  343. */
  344. public function disconnect()
  345. {
  346. $message = '</stream:stream>';
  347. // If the stream isn't set, get one. Seems unlikely that we'd want to be
  348. // disconnecting when no connection is open via a stream, but it saves us
  349. // having to go through the rigormoral of actually setting up a proper, full
  350. // mock connection.
  351. if (!isset($this->_stream)) {
  352. $this->_stream = $this->getStream($this->getServer());
  353. }
  354. $this->_stream->send($message);
  355. $this->_stream->disconnect();
  356. $this->_logger->debug('Disconnected');
  357. return true;
  358. }
  359. /**
  360. * Establish the start of a session.
  361. *
  362. * @return boolean
  363. */
  364. public function establishSession()
  365. {
  366. // Send message requesting start of session.
  367. $message = "<iq to='" . $this->_realm . "' type='set' id='sess_1'>"
  368. . "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>"
  369. . "</iq>";
  370. $this->_stream->send($message);
  371. // Should now get an iq in response from the server to say the session
  372. // was established.
  373. $response = $this->waitForServer('iq');
  374. $this->_logger->debug('Received: ' . $response->asXML());
  375. return true;
  376. }
  377. /**
  378. * Get the last response as an instance of Xmpp_Iq.
  379. *
  380. * @return Xmpp_Iq
  381. */
  382. public function getIq()
  383. {
  384. if ((string) $this->_lastResponse->getName() != 'iq') {
  385. throw new Xmpp_Exception('Last stanza received was not an iq stanza');
  386. }
  387. return new Xmpp_Iq($this->_lastResponse);
  388. }
  389. /**
  390. * Get the last response an an instance of Xmpp_Message.
  391. *
  392. * @return Xmpp_Message
  393. */
  394. public function getMessage()
  395. {
  396. if ((string) $this->_lastResponse->getName() != 'message') {
  397. throw new Xmpp_Exception('Last stanza received was not a message');
  398. }
  399. return new Xmpp_Message($this->_lastResponse);
  400. }
  401. /**
  402. * Set the presence of the user.
  403. *
  404. * @param string $status Custom status string
  405. * @param string $show Current state of user, e.g. away, do not disturb
  406. * @param int $priority Presence priority
  407. *
  408. * @todo Allow multiple statuses to be entered
  409. *
  410. * @return boolean
  411. */
  412. public function presence($status = null, $show = null, $priority = null)
  413. {
  414. if (is_null($status) && is_null($show) && is_null($priority)) {
  415. $message = '<presence/>';
  416. } else {
  417. $message = "<presence xml:lang='en'>";
  418. if (!is_null($status)) {
  419. $message .= '<status>' . $status . '</status>';
  420. }
  421. if (!is_null($show) && ($show == self::PRESENCE_AWAY
  422. || $show == self::PRESENCE_CHAT || $show == self::PRESENCE_DND
  423. || $show == self::PRESENCE_XA)
  424. ) {
  425. $message .= '<show>' . $show . '</show>';
  426. }
  427. if (!is_null($priority) && is_int($priority)) {
  428. $message .= '<priority>' . $priority . '</priority>';
  429. }
  430. $message .= '</presence>';
  431. }
  432. $this->_stream->send($message);
  433. return true;
  434. }
  435. /**
  436. * Wait for the server to respond.
  437. *
  438. * @todo Get this to return after a timeout period if nothing has come back
  439. *
  440. * @return string
  441. */
  442. public function wait()
  443. {
  444. // Wait for any tag to be sent by the server
  445. $response = $this->waitForServer('*');
  446. // Store the last response
  447. $this->_lastResponse = $response;
  448. if ($response !== false) {
  449. $tag = $this->_lastResponse->getName();
  450. } else {
  451. $tag = null;
  452. }
  453. // Return what type of tag has come back
  454. return $tag;
  455. }
  456. /**
  457. * Check if server supports the Multi-User Chat extension.
  458. *
  459. * @return boolean
  460. */
  461. public function isMucSupported()
  462. {
  463. // Set up return value. Assume MUC isn't supported
  464. $mucSupported = false;
  465. // If items is empty then we haven't yet asked the server what items are
  466. // associated with it. Query the server for what items are available.
  467. if (is_null($this->items)) {
  468. $this->discoverItems();
  469. }
  470. // Iterate over the items and the main server to ask if MUC is supported
  471. $items = $this->items;
  472. $items[] = array('jid' => $this->_realm);
  473. foreach ($items as $item) {
  474. // Send iq stanza asking if this server supports MUC.
  475. $message = "<iq from='" . $this->_userName . '@' . $this->_realm . '/'
  476. . $this->_resource . "' id='disco1' "
  477. . "to='" . $item['jid'] . "' type='get'>"
  478. . "<query xmlns='http://jabber.org/protocol/disco#info'/>"
  479. . "</iq>";
  480. $this->_stream->send($message);
  481. $this->_logger->debug('Querying for MUC support');
  482. // Wait for iq response
  483. $response = false;
  484. while (!$response) {
  485. $response = $this->waitForServer('iq');
  486. }
  487. $this->_logger->debug('Received: ' . $response->asXML());
  488. // Check if feature tag with appropriate var value is in response.
  489. // If it is, then MUC is supported
  490. if (isset($response->query)) {
  491. foreach ($response->query->children() as $feature) {
  492. if ($feature->getName() == 'feature'
  493. && isset($feature->attributes()->var)
  494. && $feature->attributes()->var == 'http://jabber.org/protocol/muc'
  495. ) {
  496. $mucSupported = true;
  497. }
  498. }
  499. }
  500. }
  501. return $mucSupported;
  502. }
  503. /**
  504. * Join a MUC Room.
  505. *
  506. * @param type $roomJid Room to join.
  507. * @param type $nick Nickname to join as.
  508. * @param type $overRideReservedNick Override the server assigned nickname.
  509. *
  510. * @return boolean
  511. */
  512. public function join($roomJid, $nick, $overRideReservedNick = false)
  513. {
  514. // If we shouldn't over ride the reserved nick, check to see if one is
  515. // set.
  516. if (!$overRideReservedNick) {
  517. // Make a request to see if we have a reserved nick name in the room
  518. // that we want to join.
  519. $reservedNick = $this->requestReservedNickname($roomJid);
  520. if (!is_null($reservedNick)) {
  521. $nick = $reservedNick;
  522. }
  523. }
  524. // Attempt to enter the room by sending it a presence element.
  525. $message = "<presence from='" . $this->_userName . '@' . $this->_realm
  526. . '/' . $this->_resource . "' to='" . $roomJid . '/' . $nick
  527. . "'><x xmlns='http://jabber.org/protocol/muc'/></presence>";
  528. $this->_stream->send($message);
  529. $this->_logger->debug('Attempting to join the room ' . $roomJid);
  530. // Should now get a list of presences back containing the details of all the
  531. // other occupants of the room.
  532. $response = false;
  533. while (!$response) {
  534. $response = $this->waitForServer('presence');
  535. }
  536. $this->_logger->debug('Received: ' . $response->asXML());
  537. // Room has now been joined, if it isn't the array of joinedRooms, add it
  538. if (!in_array($roomJid, $this->joinedRooms)) {
  539. $this->joinedRooms[] = $roomJid;
  540. }
  541. return true;
  542. }
  543. /**
  544. * Sends a message.
  545. *
  546. * @param string $to Intended recipient of the message.
  547. * @param string $text Contents of the message.
  548. *
  549. * @return boolean
  550. */
  551. public function message($to, $text)
  552. {
  553. // Get the first part of the JID
  554. $firstPart = array_shift(explode('/', $to));
  555. if (in_array($firstPart, $this->joinedRooms)) {
  556. $type = 'groupchat';
  557. $to = $firstPart;
  558. } else {
  559. $type = 'normal';
  560. }
  561. $message = "<message to='" . $to . "' from='" . $this->_userName . '@'
  562. . $this->_realm . '/' . $this->_resource . "' type='" . $type
  563. . "' xml:lang='en'><body>" . $this->encode($text)
  564. . "</body></message>";
  565. $this->_stream->send($message);
  566. return true;
  567. }
  568. /**
  569. * Send a ping to the server.
  570. *
  571. * @return boolean
  572. */
  573. public function ping($to)
  574. {
  575. $message = "<iq to='" . $to . "' from='" . $this->_userName . '@'
  576. . $this->_realm . '/' . $this->_resource . "' type='get' "
  577. . "id='" . uniqid() . "'>"
  578. . "<ping xmlns='urn:xmpp:ping'/>"
  579. . "</iq>";
  580. $this->_stream->send($message);
  581. return true;
  582. }
  583. /**
  584. * Send a response to a ping.
  585. *
  586. * @param string $to Who the response is being sent back to.
  587. * @param string $id The ID from the original ping.
  588. *
  589. * @return boolean
  590. */
  591. public function pong($to, $id)
  592. {
  593. $message = "<iq from='" . $this->_userName . '@' . $this->_realm . '/'
  594. . $this->_resource . "' to='" . $to . "' id='" . $id . "' "
  595. . "type='result'/>";
  596. $this->_stream->send($message);
  597. return true;
  598. }
  599. /**
  600. * Class destructor. Will try and close the connection if it is open
  601. */
  602. public function __destruct()
  603. {
  604. if (!is_null($this->_stream) && $this->_stream->isConnected()) {
  605. $this->_stream->send('</stream:stream>');
  606. $this->_logger->debug('Stream closed');
  607. }
  608. }
  609. /**
  610. * Get the server this class should connect to.
  611. *
  612. * @return string
  613. */
  614. protected function getServer()
  615. {
  616. return 'tcp://' . $this->_host . ':' . $this->_port;
  617. }
  618. /**
  619. * Gets a Stream object that encapsulates the actual connection to the
  620. * server
  621. *
  622. * @param string $remoteSocket Address to connect to
  623. * @param int $timeOut Length of time to wait for connection
  624. * @param int $flags Flags to be set on the connection
  625. * @param resource $context Context of the connection
  626. *
  627. * @return Stream
  628. */
  629. protected function getStream(
  630. $remoteSocket, $timeOut = null, $flags = null, $context = null
  631. ) {
  632. return new Stream($remoteSocket, $timeOut, $flags, $context);
  633. }
  634. /**
  635. * Gets the logging class
  636. *
  637. * @param int $logLevel Logging level to be used
  638. *
  639. * @return Zend_Log
  640. */
  641. protected function getLogger($logLevel)
  642. {
  643. $writer = new Zend_Log_Writer_Stream('php://output');
  644. return new Zend_Log($writer);
  645. }
  646. /**
  647. * Checks if a given authentication mechanism is available.
  648. *
  649. * @param string $mechanism Mechanism to check availability for.
  650. *
  651. * @return boolean
  652. */
  653. protected function mechanismAvailable($mechanism)
  654. {
  655. return in_array($mechanism, $this->_mechanisms);
  656. }
  657. /**
  658. * Discovers what items (basically features) are available on the server.
  659. *
  660. * @return void
  661. */
  662. protected function discoverItems()
  663. {
  664. // Send IQ stanza asking server what items are associated with it.
  665. $message = "<iq from='" . $this->_userName . '@' . $this->_realm . '/'
  666. . $this->_resource . "' id='" . uniqid() . "' "
  667. . "to='" . $this->_realm . "' type='get'>"
  668. . "<query xmlns='http://jabber.org/protocol/disco#items'/>"
  669. . '</iq>';
  670. $this->_stream->send($message);
  671. $this->_logger->debug('Querying for available services');
  672. // Wait for iq response
  673. $response = false;
  674. while (!$response || $response->getName() != 'iq'
  675. || strpos($response->asXml(), '<item') === false
  676. ) {
  677. $response = $this->waitForServer('iq');
  678. }
  679. $this->_logger->debug('Received: ' . $response->asXML());
  680. // Check if query tag is in response. If it is, then iterate over the
  681. // children to get the items available.
  682. if (isset($response->query)) {
  683. foreach ($response->query->children() as $item) {
  684. if ($item->getName() == 'item'
  685. && isset($item->attributes()->jid)
  686. && isset($item->attributes()->name)
  687. ) {
  688. // If items is null then we need to turn it into an array.
  689. if (is_null($this->items)) {
  690. $this->items = array();
  691. }
  692. $this->items[] = array(
  693. 'jid' => $item->attributes()->jid,
  694. 'name' => $item->attributes()->name,
  695. );
  696. }
  697. }
  698. }
  699. }
  700. /**
  701. * Encodes text for XML.
  702. *
  703. * Most inspired by this example:
  704. * http://www.sourcerally.net/Scripts/39-Convert-HTML-Entities-to-XML-Entities
  705. *
  706. * @param string $text
  707. * @return string
  708. */
  709. private function encode($text)
  710. {
  711. $text = htmlentities($text, ENT_COMPAT, 'UTF-8');
  712. $xml = array(
  713. '&#34;','&#38;','&#38;','&#60;','&#62;','&#160;','&#161;','&#162;',
  714. '&#163;','&#164;','&#165;','&#166;','&#167;','&#168;','&#169;','&#170;',
  715. '&#171;','&#172;','&#173;','&#174;','&#175;','&#176;','&#177;','&#178;',
  716. '&#179;','&#180;','&#181;','&#182;','&#183;','&#184;','&#185;','&#186;',
  717. '&#187;','&#188;','&#189;','&#190;','&#191;','&#192;','&#193;','&#194;',
  718. '&#195;','&#196;','&#197;','&#198;','&#199;','&#200;','&#201;','&#202;',
  719. '&#203;','&#204;','&#205;','&#206;','&#207;','&#208;','&#209;','&#210;',
  720. '&#211;','&#212;','&#213;','&#214;','&#215;','&#216;','&#217;','&#218;',
  721. '&#219;','&#220;','&#221;','&#222;','&#223;','&#224;','&#225;','&#226;',
  722. '&#227;','&#228;','&#229;','&#230;','&#231;','&#232;','&#233;','&#234;',
  723. '&#235;','&#236;','&#237;','&#238;','&#239;','&#240;','&#241;','&#242;',
  724. '&#243;','&#244;','&#245;','&#246;','&#247;','&#248;','&#249;','&#250;',
  725. '&#251;','&#252;','&#253;','&#254;','&#255;','&#338;','&#339;','&#352;',
  726. '&#353;','&#376;','&#402;','&#710;','&#732;','&#913;','&#914;','&#915;',
  727. '&#916;','&#917;','&#918;','&#919;','&#920;','&#921;','&#922;','&#923;',
  728. '&#924;','&#925;','&#926;','&#927;','&#928;','&#929;','&#931;','&#932;',
  729. '&#933;','&#934;','&#935;','&#936;','&#937;','&#945;','&#946;','&#947;',
  730. '&#948;','&#949;','&#950;','&#951;','&#952;','&#953;','&#954;','&#955;',
  731. '&#956;','&#957;','&#958;','&#959;','&#960;','&#961;','&#962;','&#963;',
  732. '&#964;','&#965;','&#966;','&#967;','&#968;','&#969;','&#977;','&#978;',
  733. '&#982;','&#8194;','&#8195;','&#8201;','&#8204;','&#8205;','&#8206;',
  734. '&#8207;','&#8211;','&#8212;','&#8216;','&#8217;','&#8218;','&#8220;',
  735. '&#8221;','&#8222;','&#8224;','&#8225;','&#8226;','&#8230;','&#8240;',
  736. '&#8242;','&#8243;','&#8249;','&#8250;','&#8254;','&#8364;','&#8482;',
  737. '&#8592;','&#8593;','&#8594;','&#8595;','&#8596;','&#8629;','&#8704;',
  738. '&#8706;','&#8707;','&#8709;','&#8711;','&#8712;','&#8713;','&#8715;',
  739. '&#8719;','&#8721;','&#8722;','&#8727;','&#8730;','&#8733;','&#8734;',
  740. '&#8736;','&#8743;','&#8744;','&#8745;','&#8746;','&#8747;','&#8756;',
  741. '&#8764;','&#8773;','&#8776;','&#8800;','&#8801;','&#8804;','&#8805;',
  742. '&#8834;','&#8835;','&#8836;','&#8838;','&#8839;','&#8853;','&#8855;',
  743. '&#8869;','&#8901;','&#8968;','&#8969;','&#8970;','&#8971;','&#9674;',
  744. '&#9824;','&#9827;','&#9829;','&#9830;',
  745. );
  746. $html = array(
  747. '&quot;','&amp;','&amp;','&lt;','&gt;','&nbsp;','&iexcl;','&cent;',
  748. '&pound;','&curren;','&yen;','&brvbar;','&sect;','&uml;','&copy;',
  749. '&ordf;','&laquo;','&not;','&shy;','&reg;','&macr;','&deg;','&plusmn;',
  750. '&sup2;','&sup3;','&acute;','&micro;','&para;','&middot;','&cedil;',
  751. '&sup1;','&ordm;','&raquo;','&frac14;','&frac12;','&frac34;','&iquest;',
  752. '&Agrave;','&Aacute;','&Acirc;','&Atilde;','&Auml;','&Aring;','&AElig;',
  753. '&Ccedil;','&Egrave;','&Eacute;','&Ecirc;','&Euml;','&Igrave;',
  754. '&Iacute;','&Icirc;','&Iuml;','&ETH;','&Ntilde;','&Ograve;','&Oacute;',
  755. '&Ocirc;','&Otilde;','&Ouml;','&times;','&Oslash;','&Ugrave;','&Uacute;',
  756. '&Ucirc;','&Uuml;','&Yacute;','&THORN;','&szlig;','&agrave;','&aacute;',
  757. '&acirc;','&atilde;','&auml;','&aring;','&aelig;','&ccedil;','&egrave;',
  758. '&eacute;','&ecirc;','&euml;','&igrave;','&iacute;','&icirc;','&iuml;',
  759. '&eth;','&ntilde;','&ograve;','&oacute;','&ocirc;','&otilde;','&ouml;',
  760. '&divide;','&oslash;','&ugrave;','&uacute;','&ucirc;','&uuml;',
  761. '&yacute;','&thorn;','&yuml;', '&OElig;','&oelig;','&Scaron;','&scaron;',
  762. '&Yuml;','&fnof;','&circ;','&tilde;','&Alpha;','&Beta;','&Gamma;',
  763. '&Delta;','&Epsilon;','&Zeta;','&Eta;','&Theta;','&Iota;','&Kappa;',
  764. '&Lambda;','&Mu;','&Nu;','&Xi;','&Omicron;','&Pi;','&Rho;','&Sigma;',
  765. '&Tau;','&Upsilon;','&Phi;','&Chi;','&Psi;','&Omega;','&alpha;','&beta;',
  766. '&gamma;','&delta;','&epsilon;','&zeta;','&eta;','&theta;','&iota;',
  767. '&kappa;','&lambda;','&mu;','&nu;','&xi;','&omicron;','&pi;','&rho;',
  768. '&sigmaf;','&sigma;','&tau;','&upsilon;','&phi;','&chi;','&psi;',
  769. '&omega;','&thetasym;','&upsih;','&piv;','&ensp;','&emsp;','&thinsp;',
  770. '&zwnj;','&zwj;','&lrm;','&rlm;','&ndash;','&mdash;','&lsquo;','&rsquo;',
  771. '&sbquo;','&ldquo;','&rdquo;','&bdquo;','&dagger;','&Dagger;','&bull;',
  772. '&hellip;','&permil;','&prime;','&Prime;','&lsaquo;','&rsaquo;',
  773. '&oline;','&euro;','&trade;','&larr;','&uarr;','&rarr;','&darr;',
  774. '&harr;','&crarr;','&forall;','&part;','&exist;','&empty;','&nabla;',
  775. '&isin;','&notin;','&ni;','&prod;','&sum;','&minus;','&lowast;',
  776. '&radic;','&prop;','&infin;','&ang;','&and;','&or;','&cap;','&cup;',
  777. '&int;','&there4;','&sim;','&cong;','&asymp;','&ne;','&equiv;','&le;',
  778. '&ge;','&sub;','&sup;','&nsub;','&sube;','&supe;','&oplus;','&otimes;',
  779. '&perp;','&sdot;','&lceil;','&rceil;','&lfloor;','&rfloor;','&loz;',
  780. '&spades;','&clubs;','&hearts;','&diams;',
  781. );
  782. $text = str_replace($html, $xml, $text);
  783. $text = str_ireplace($html, $xml, $text);
  784. return $text;
  785. }
  786. /**
  787. * Checks if the server has a reserved nickname for this user in the given room.
  788. *
  789. * @param string $roomJid Given room the check the reserved nicknames for.
  790. *
  791. * @return string
  792. */
  793. protected function requestReservedNickname($roomJid)
  794. {
  795. $message = "<iq from='" . $this->_userName . '@' . $this->_realm . '/'
  796. . $this->_resource . "' id='" . uniqid() . "' "
  797. . "to='" . $roomJid . "' type='get'>"
  798. . "<query xmlns='http://jabber.org/protocol/disco#info' "
  799. . "node='x-roomuser-item'/></iq>";
  800. $this->_stream->send($message);
  801. $this->_logger->debug('Querying for reserved nickname in ' . $roomJid);
  802. // Wait for iq response
  803. $response = false;
  804. while (!$response) {
  805. $response = $this->waitForServer('iq');
  806. }
  807. $this->_logger->debug('Received: ' . $response->asXML());
  808. // If query isn't empty then the user does have a reserved nickname.
  809. if (isset($response->query) && count($response->query->children()) > 0
  810. && isset($response->query->identity)
  811. ) {
  812. $reservedNick = $response->query->identity->attributes()->name;
  813. } else {
  814. $reservedNick = null;
  815. }
  816. return $reservedNick;
  817. }
  818. /**
  819. * Take the given features tag and figure out what authentication mechanisms are
  820. * supported from it's contents.
  821. *
  822. * @param SimpleXMLElement $features <stream:features> saying what server
  823. * supports
  824. *
  825. * @return void
  826. */
  827. protected function setMechanisms(SimpleXMLElement $features)
  828. {
  829. // Set up an array to hold any matches
  830. $matches = array();
  831. // A response containing a stream:features tag should have been passed in.
  832. // That should contain a mechanisms tag. Find the mechanisms tag and load it
  833. // into a SimpleXMLElement object.
  834. if (preg_match('/<stream:features.*(<mechanisms.*<\/mechanisms>).*<\/stream:features>/', $features->asXml(), $matches) != 0) {
  835. // Clear out any existing mechanisms
  836. $this->_mechanisms = array();
  837. // Create SimpleXMLElement
  838. $xml = simplexml_load_string($matches[1]);
  839. foreach ($xml->children() as $child) {
  840. $this->_mechanisms[] = (string) $child;
  841. }
  842. }
  843. }
  844. /**
  845. * Starts an XMPP connection.
  846. *
  847. * @return void
  848. */
  849. protected function startStream()
  850. {
  851. $message = '<stream:stream to="' . $this->_host . '" '
  852. . 'xmlns:stream="http://etherx.jabber.org/streams" '
  853. . 'xmlns="jabber:client" version="1.0">';
  854. $this->_stream->send($message);
  855. $this->_logger->debug('Stream started');
  856. }
  857. /**
  858. * Waits for the server to send the specified tag back.
  859. *
  860. * @param string $tag Tag to wait for from the server.
  861. *
  862. * @return boolean|SimpleXMLElement
  863. */
  864. protected function waitForServer($tag)
  865. {
  866. $this->_logger->debug("Tag we're waiting for: " . $tag);
  867. $fromServer = false;
  868. // If there is nothing left in the buffer, wait for the stream to update
  869. if (count($this->_buffer) == 0 && $this->_stream->select() > 0) {
  870. $response = '';
  871. $done = false;
  872. // Read data from the connection.
  873. while (!$done) {
  874. $response .= $this->_stream->read(4096);
  875. if ($this->_stream->select() == 0) {
  876. $done = true;
  877. }
  878. }
  879. $this->_logger->debug('Response (Xmpp_Connection): ' . $response);
  880. // If the response isn't empty, load it into a SimpleXML element
  881. if (trim($response) != '') {
  882. // If the response from the server starts (where "starts
  883. // with" means "appears after the xml prologue if one is
  884. // present") with "<stream:stream and it doesn't have a
  885. // closing "</stream:stream>" then we should append one so
  886. // that it can be easily loaded into a SimpleXMLElement,
  887. // otherwise it will cause an error to be thrown because of
  888. // malformed XML.
  889. // Check if response starts with XML Prologue:
  890. if (preg_match("/^<\?xml version='1.0'( encoding='UTF-8')?\?>/", $response, $matches) == 1) {
  891. $offset = strlen($matches[0]);
  892. $prologue = $matches[0];
  893. } else {
  894. $offset = 0;
  895. }
  896. // Check if first part of the actual response starts with
  897. // <stream:stream
  898. if (strpos($response, '<stream:stream ') === $offset) {
  899. // If so, append a closing tag
  900. $response .= '</stream:stream>';
  901. }
  902. // For consistent handling and correct stream namespace
  903. // support, we should wrap all responses in the
  904. // stream:stream tags to make sure everything works as
  905. // expected. Unless the response already contains such tags.
  906. if (strpos($response, '<stream:stream') === false) {
  907. $response = '<stream:stream '
  908. . 'xmlns:stream="http://etherx.jabber.org/streams" '
  909. . "xmlns:ack='http://www.xmpp.org/extensions/xep-0198.html#ns' "
  910. . 'xmlns="jabber:client" '
  911. . 'from="' . $this->_realm . '" '
  912. . 'xml:lang="en" version="1.0">'
  913. . $response . '</stream:stream>';
  914. }
  915. // If the xml prologue should be at the start, move it
  916. // because it will now be in the wrong place. We can assume
  917. // if $offset is not 0 that there was a prologue.
  918. if ($offset != 0) {
  919. $response = $prologue
  920. . str_replace($prologue, '', $response);
  921. }
  922. $xml = simplexml_load_string($response);
  923. // If we want the stream element itself, just return that,
  924. // otherwise check the contents of the stream.
  925. if ($tag == 'stream:stream') {
  926. $fromServer = $xml;
  927. } else if ($xml instanceof SimpleXMLElement
  928. && $xml->getName() == 'stream'
  929. ) {
  930. // Get the namespaces used at the root level of the
  931. // document. Add a blank namespace on for anything that
  932. // isn't namespaced. Then we can iterate over all of the
  933. // elements in the doc.
  934. $namespaces = $xml->getNamespaces();
  935. $namespaces['blank'] = '';
  936. foreach ($namespaces as $namespace) {
  937. foreach ($xml->children($namespace) as $child) {
  938. if ($child instanceof SimpleXMLElement) {
  939. $this->_buffer[] = $child;
  940. }
  941. }
  942. }
  943. }
  944. }
  945. }
  946. $this->_logger->debug('Contents of $fromServer: ' . var_export($fromServer, true));
  947. $this->_logger->debug('Contents of $this->_buffer before foreach: ' . var_export($this->_buffer, true));
  948. // Now go over what is in the buffer and return anything necessary
  949. foreach ($this->_buffer as $key => $stanza) {
  950. // Only bother looking for more tags if one has not yet been found.
  951. if ($fromServer === false) {
  952. // Remove this element from the buffer because we do not want it to
  953. // be processed again.
  954. unset($this->_buffer[$key]);
  955. // If this the tag we want, save it for returning.
  956. if ($tag == '*' || $stanza->getName() == $tag) {
  957. $fromServer = $stanza;
  958. }
  959. }
  960. }
  961. $this->_logger->debug('Contents of $this->_buffer after foreach: ' . var_export($this->_buffer, true));
  962. return $fromServer;
  963. }
  964. }