PageRenderTime 54ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/pichi-php/XMPP/XMLStream.php

http://pichi.googlecode.com/
PHP | 768 lines | 433 code | 47 blank | 288 comment | 100 complexity | 61a3a064e00234707a2f05a1749a98c7 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * XMPPHP: The PHP XMPP Library
  4. * Copyright (C) 2008 Nathanael C. Fritz
  5. * This file is part of SleekXMPP.
  6. *
  7. * XMPPHP is free software; you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation; either version 2 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * XMPPHP is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with XMPPHP; if not, write to the Free Software
  19. * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  20. *
  21. * @category xmpphp
  22. * @package XMPPHP
  23. * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
  24. * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
  25. * @author Michael Garvin <JID: gar@netflint.net>
  26. * @author Alexey Kasyanchuk <JID: degx@jabber.ru>
  27. * @copyright 2009 Nathanael C. Fritz
  28. */
  29. /** XMPPHP_Exception */
  30. require_once dirname(__FILE__) . '/Exception.php';
  31. /** XMPPHP_XMLObj */
  32. require_once dirname(__FILE__) . '/XMLObj.php';
  33. /** XMPPHP_Log */
  34. require_once dirname(__FILE__) . '/Log.php';
  35. /**
  36. * XMPPHP XML Stream
  37. *
  38. * @category xmpphp
  39. * @package XMPPHP
  40. * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
  41. * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
  42. * @author Michael Garvin <JID: gar@netflint.net>
  43. * @author Alexey Kasyanchuk <JID: degx@jabber.ru>
  44. * @copyright 2009 Nathanael C. Fritz
  45. * @version $Id$
  46. */
  47. class XMPPHP_XMLStream {
  48. /**
  49. * @var resource
  50. */
  51. protected $socket;
  52. /**
  53. * @var resource
  54. */
  55. protected $parser;
  56. /**
  57. * @var string
  58. */
  59. protected $buffer;
  60. /**
  61. * @var integer
  62. */
  63. protected $xml_depth = 0;
  64. /**
  65. * @var string
  66. */
  67. protected $host;
  68. /**
  69. * @var integer
  70. */
  71. protected $port;
  72. /**
  73. * @var string
  74. */
  75. protected $stream_start = '<stream>';
  76. /**
  77. * @var string
  78. */
  79. protected $stream_end = '</stream>';
  80. /**
  81. * @var boolean
  82. */
  83. protected $disconnected = false;
  84. /**
  85. * @var boolean
  86. */
  87. protected $sent_disconnect = false;
  88. /**
  89. * @var array
  90. */
  91. protected $ns_map = array();
  92. /**
  93. * @var array
  94. */
  95. protected $current_ns = array();
  96. /**
  97. * @var array
  98. */
  99. protected $xmlobj = null;
  100. /**
  101. * @var array
  102. */
  103. protected $nshandlers = array();
  104. /**
  105. * @var array
  106. */
  107. protected $xpathhandlers = array();
  108. /**
  109. * @var array
  110. */
  111. protected $idhandlers = array();
  112. /**
  113. * @var array
  114. */
  115. protected $eventhandlers = array();
  116. /**
  117. * @var integer
  118. */
  119. protected $lastid = 0;
  120. /**
  121. * @var string
  122. */
  123. protected $default_ns;
  124. /**
  125. * @var string
  126. */
  127. protected $until = '';
  128. /**
  129. * @var string
  130. */
  131. protected $until_count = '';
  132. /**
  133. * @var array
  134. */
  135. protected $until_happened = false;
  136. /**
  137. * @var array
  138. */
  139. protected $until_payload = array();
  140. /**
  141. * @var XMPPHP_Log
  142. */
  143. protected $log;
  144. /**
  145. * @var boolean
  146. */
  147. protected $reconnect = true;
  148. /**
  149. * @var boolean
  150. */
  151. protected $been_reset = false;
  152. /**
  153. * @var boolean
  154. */
  155. protected $is_server;
  156. /**
  157. * @var float
  158. */
  159. protected $last_send = 0;
  160. /**
  161. * @var boolean
  162. */
  163. protected $use_ssl = false;
  164. /**
  165. * @var integer
  166. */
  167. protected $reconnectTimeout = 30;
  168. /**
  169. * Constructor
  170. *
  171. * @param string $host
  172. * @param string $port
  173. * @param boolean $printlog
  174. * @param string $loglevel
  175. * @param boolean $is_server
  176. */
  177. public function __construct($host = null, $port = null, $printlog = false, $loglevel = null, $is_server = false) {
  178. $this->reconnect = !$is_server;
  179. $this->is_server = $is_server;
  180. $this->host = $host;
  181. $this->port = $port;
  182. $this->setupParser();
  183. $this->log = new XMPPHP_Log($printlog, $loglevel);
  184. }
  185. /**
  186. * Destructor
  187. * Cleanup connection
  188. */
  189. public function __destruct() {
  190. if(!$this->disconnected && $this->socket) {
  191. $this->disconnect();
  192. }
  193. }
  194. /**
  195. * Return the log instance
  196. *
  197. * @return XMPPHP_Log
  198. */
  199. public function getLog() {
  200. return $this->log;
  201. }
  202. /**
  203. * Get next ID
  204. *
  205. * @return integer
  206. */
  207. public function getId() {
  208. $this->lastid++;
  209. return $this->lastid;
  210. }
  211. /**
  212. * Set SSL
  213. *
  214. * @return integer
  215. */
  216. public function useSSL($use=true) {
  217. $this->use_ssl = $use;
  218. }
  219. /**
  220. * Add ID Handler
  221. *
  222. * @param integer $id
  223. * @param string $pointer
  224. * @param string $obj
  225. */
  226. public function addIdHandler($id, $pointer, $obj = null) {
  227. $this->idhandlers[$id] = array($pointer, $obj);
  228. }
  229. /**
  230. * Add Handler
  231. *
  232. * @param string $name
  233. * @param string $ns
  234. * @param string $pointer
  235. * @param string $obj
  236. * @param integer $depth
  237. */
  238. public function addHandler($name, $ns, $pointer, $obj = null, $depth = 1) {
  239. #TODO deprication warning
  240. $this->nshandlers[] = array($name,$ns,$pointer,$obj, $depth);
  241. }
  242. /**
  243. * Add XPath Handler
  244. *
  245. * @param string $xpath
  246. * @param string $pointer
  247. * @param
  248. */
  249. public function addXPathHandler($xpath, $pointer, $obj = null) {
  250. if (preg_match_all("/\(?{[^\}]+}\)?(\/?)[^\/]+/", $xpath, $regs)) {
  251. $ns_tags = $regs[0];
  252. } else {
  253. $ns_tags = array($xpath);
  254. }
  255. foreach($ns_tags as $ns_tag) {
  256. list($l, $r) = explode("}", $ns_tag);
  257. if ($r != null) {
  258. $xpart = array(substr($l, 1), $r);
  259. } else {
  260. $xpart = array(null, $l);
  261. }
  262. $xpath_array[] = $xpart;
  263. }
  264. $this->xpathhandlers[] = array($xpath_array, $pointer, $obj);
  265. }
  266. /**
  267. * Add Event Handler
  268. *
  269. * @param integer $id
  270. * @param string $pointer
  271. * @param string $obj
  272. */
  273. public function addEventHandler($name, $pointer, $obj) {
  274. $this->eventhandlers[] = array($name, $pointer, $obj);
  275. }
  276. /**
  277. * Connect to XMPP Host
  278. *
  279. * @param integer $timeout
  280. * @param boolean $persistent
  281. * @param boolean $sendinit
  282. */
  283. public function connect($timeout = 30, $persistent = false, $sendinit = true) {
  284. $this->sent_disconnect = false;
  285. $starttime = time();
  286. do {
  287. $this->disconnected = false;
  288. $this->sent_disconnect = false;
  289. if($persistent) {
  290. $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
  291. } else {
  292. $conflag = STREAM_CLIENT_CONNECT;
  293. }
  294. $conntype = 'tcp';
  295. if($this->use_ssl) $conntype = 'ssl';
  296. $this->log->log("Connecting to $conntype://{$this->host}:{$this->port}");
  297. try {
  298. $this->socket = @stream_socket_client("$conntype://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag);
  299. } catch (Exception $e) {
  300. throw new XMPPHP_Exception($e->getMessage());
  301. }
  302. if(!$this->socket) {
  303. $this->log->log("Could not connect.", XMPPHP_Log::LEVEL_ERROR);
  304. $this->disconnected = true;
  305. # Take it easy for a few seconds
  306. sleep(min($timeout, 5));
  307. }
  308. } while (!$this->socket && (time() - $starttime) < $timeout);
  309. if ($this->socket) {
  310. stream_set_blocking($this->socket, 1);
  311. if($sendinit) $this->send($this->stream_start);
  312. } else {
  313. throw new XMPPHP_Exception("Could not connect before timeout.");
  314. }
  315. }
  316. /**
  317. * Reconnect XMPP Host
  318. */
  319. public function doReconnect() {
  320. if(!$this->is_server) {
  321. $this->log->log("Reconnecting ($this->reconnectTimeout)...", XMPPHP_Log::LEVEL_WARNING);
  322. $this->connect($this->reconnectTimeout, false, false);
  323. $this->reset();
  324. $this->event('reconnect');
  325. }
  326. }
  327. public function setReconnectTimeout($timeout) {
  328. $this->reconnectTimeout = $timeout;
  329. }
  330. /**
  331. * Disconnect from XMPP Host
  332. */
  333. public function disconnect() {
  334. $this->log->log("Disconnecting...", XMPPHP_Log::LEVEL_VERBOSE);
  335. if(false == (bool) $this->socket) {
  336. return;
  337. }
  338. $this->reconnect = false;
  339. $this->send($this->stream_end);
  340. $this->sent_disconnect = true;
  341. $this->processUntil('end_stream', 5);
  342. $this->disconnected = true;
  343. }
  344. /**
  345. * Are we are disconnected?
  346. *
  347. * @return boolean
  348. */
  349. public function isDisconnected() {
  350. return $this->disconnected;
  351. }
  352. /**
  353. * Core reading tool
  354. * 0 -> only read if data is immediately ready
  355. * NULL -> wait forever and ever
  356. * integer -> process for this amount of time
  357. */
  358. private function __process($maximum=5) {
  359. $remaining = $maximum;
  360. do {
  361. $starttime = (microtime(true) * 1000000);
  362. $read = array($this->socket);
  363. $write = array();
  364. $except = array();
  365. if (is_null($maximum)) {
  366. $secs = NULL;
  367. $usecs = NULL;
  368. } else if ($maximum == 0) {
  369. $secs = 0;
  370. $usecs = 0;
  371. } else {
  372. $usecs = $remaining % 1000000;
  373. $secs = floor(($remaining - $usecs) / 1000000);
  374. }
  375. if($secs < 1)
  376. $updated = @stream_select($read, $write, $except, 1);
  377. else
  378. $updated = @stream_select($read, $write, $except, $secs, $usecs);
  379. if ($updated === false) {
  380. $this->log->log("Error on stream_select()", XMPPHP_Log::LEVEL_VERBOSE);
  381. if ($this->reconnect) {
  382. $this->doReconnect();
  383. } else {
  384. fclose($this->socket);
  385. $this->socket = NULL;
  386. return false;
  387. }
  388. } else if ($updated > 0) {
  389. # XXX: Is this big enough?
  390. $buff = @fread($this->socket, 4096);
  391. if(!$buff) {
  392. if($this->reconnect) {
  393. $this->doReconnect();
  394. } else {
  395. fclose($this->socket);
  396. $this->socket = NULL;
  397. return false;
  398. }
  399. }
  400. $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
  401. $this->reciveData($buff);
  402. xml_parse($this->parser, $buff, false);
  403. } else {
  404. # $updated == 0 means no changes during timeout.
  405. }
  406. $endtime = (microtime(true)*1000000);
  407. $time_past = $endtime - $starttime;
  408. $remaining = $remaining - $time_past;
  409. $this->__post_process();
  410. } while (is_null($maximum) || $remaining > 0);
  411. return true;
  412. }
  413. protected function __post_process(){}
  414. protected function reciveData($data){} // need for data abstraction
  415. /**
  416. * Process
  417. *
  418. * @return string
  419. */
  420. public function process() {
  421. $this->__process(NULL);
  422. }
  423. /**
  424. * Process until a timeout occurs
  425. *
  426. * @param integer $timeout
  427. * @return string
  428. */
  429. public function processTime($timeout=NULL) {
  430. if (is_null($timeout)) {
  431. return $this->__process(NULL);
  432. } else {
  433. return $this->__process($timeout * 1000000);
  434. }
  435. }
  436. /**
  437. * Process until a specified event or a timeout occurs
  438. *
  439. * @param string|array $event
  440. * @param integer $timeout
  441. * @return string
  442. */
  443. public function processUntil($event, $timeout=-1) {
  444. $start = time();
  445. if(!is_array($event))
  446. $event = array($event);
  447. $this->until = $event;
  448. $this->until_count = 0;
  449. while(!$this->disconnected and $this->until_count < 1 and (time() - $start < $timeout or $timeout == -1)) {
  450. $this->__process();
  451. }
  452. if($this->until_count > 0)
  453. $payload = $this->until_payload;
  454. else
  455. $payload = array();
  456. unset($this->until_payload);
  457. return $payload;
  458. }
  459. /**
  460. * Obsolete?
  461. */
  462. public function Xapply_socket($socket) {
  463. $this->socket = $socket;
  464. }
  465. /**
  466. * XML start callback
  467. *
  468. * @see xml_set_element_handler
  469. *
  470. * @param resource $parser
  471. * @param string $name
  472. */
  473. public function startXML($parser, $name, $attr) {
  474. if($this->been_reset) {
  475. $this->been_reset = false;
  476. $this->xml_depth = 0;
  477. }
  478. $this->xml_depth++;
  479. if(array_key_exists('XMLNS', $attr)) {
  480. $this->current_ns[$this->xml_depth] = $attr['XMLNS'];
  481. } else {
  482. $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1];
  483. if(!$this->current_ns[$this->xml_depth]) $this->current_ns[$this->xml_depth] = $this->default_ns;
  484. }
  485. $ns = $this->current_ns[$this->xml_depth];
  486. foreach($attr as $key => $value) {
  487. if(strstr($key, ":")) {
  488. $key = explode(':', $key);
  489. $key = $key[1];
  490. $this->ns_map[$key] = $value;
  491. }
  492. }
  493. if(!strstr($name, ":") === false)
  494. {
  495. $name = explode(':', $name);
  496. $ns = $this->ns_map[$name[0]];
  497. $name = $name[1];
  498. }
  499. $obj = new XMPPHP_XMLObj($name, $ns, $attr);
  500. if($this->xml_depth > 1) {
  501. $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj;
  502. }
  503. $this->xmlobj[$this->xml_depth] = $obj;
  504. }
  505. /**
  506. * XML end callback
  507. *
  508. * @see xml_set_element_handler
  509. *
  510. * @param resource $parser
  511. * @param string $name
  512. */
  513. public function endXML($parser, $name) {
  514. #$this->log->log("Ending $name", XMPPHP_Log::LEVEL_DEBUG);
  515. #print "$name\n";
  516. if($this->been_reset) {
  517. $this->been_reset = false;
  518. $this->xml_depth = 0;
  519. }
  520. $this->xml_depth--;
  521. if($this->xml_depth == 1) {
  522. #clean-up old objects
  523. #$found = false; #FIXME This didn't appear to be in use --Gar
  524. foreach($this->xpathhandlers as $handler) {
  525. if (is_array($this->xmlobj) && array_key_exists(2, $this->xmlobj)) {
  526. $searchxml = $this->xmlobj[2];
  527. $nstag = array_shift($handler[0]);
  528. if (($nstag[0] == null or $searchxml->ns == $nstag[0]) and ($nstag[1] == "*" or $nstag[1] == $searchxml->name)) {
  529. foreach($handler[0] as $nstag) {
  530. if ($searchxml !== null and $searchxml->hasSub($nstag[1], $ns=$nstag[0])) {
  531. $searchxml = $searchxml->sub($nstag[1], $ns=$nstag[0]);
  532. } else {
  533. $searchxml = null;
  534. break;
  535. }
  536. }
  537. if ($searchxml !== null) {
  538. if($handler[2] === null) $handler[2] = $this;
  539. $this->log->log("Calling {$handler[1]}", XMPPHP_Log::LEVEL_DEBUG);
  540. $handler[2]->$handler[1]($this->xmlobj[2]);
  541. }
  542. }
  543. }
  544. }
  545. foreach($this->nshandlers as $handler) {
  546. if($handler[4] != 1 and array_key_exists(2, $this->xmlobj) and $this->xmlobj[2]->hasSub($handler[0])) {
  547. $searchxml = $this->xmlobj[2]->sub($handler[0]);
  548. } elseif(is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) {
  549. $searchxml = $this->xmlobj[2];
  550. }
  551. if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) {
  552. if($handler[3] === null) $handler[3] = $this;
  553. $this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG);
  554. $handler[3]->$handler[2]($this->xmlobj[2]);
  555. }
  556. }
  557. foreach($this->idhandlers as $id => $handler) {
  558. if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) {
  559. if($handler[1] === null) $handler[1] = $this;
  560. $handler[1]->$handler[0]($this->xmlobj[2]);
  561. #id handlers are only used once
  562. unset($this->idhandlers[$id]);
  563. break;
  564. }
  565. }
  566. if(is_array($this->xmlobj)) {
  567. $this->xmlobj = array_slice($this->xmlobj, 0, 1);
  568. if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMPPHP_XMLObj) {
  569. $this->xmlobj[0]->subs = null;
  570. }
  571. }
  572. unset($this->xmlobj[2]);
  573. }
  574. if($this->xml_depth == 0 and !$this->been_reset) {
  575. if(!$this->disconnected) {
  576. if(!$this->sent_disconnect) {
  577. $this->send($this->stream_end);
  578. }
  579. $this->disconnected = true;
  580. $this->sent_disconnect = true;
  581. fclose($this->socket);
  582. if($this->reconnect) {
  583. $this->doReconnect();
  584. }
  585. }
  586. $this->event('end_stream');
  587. }
  588. }
  589. /**
  590. * XML character callback
  591. * @see xml_set_character_data_handler
  592. *
  593. * @param resource $parser
  594. * @param string $data
  595. */
  596. public function charXML($parser, $data) {
  597. if(array_key_exists($this->xml_depth, $this->xmlobj)) {
  598. $this->xmlobj[$this->xml_depth]->data .= $data;
  599. }
  600. }
  601. /**
  602. * Event?
  603. *
  604. * @param string $name
  605. * @param string $payload
  606. */
  607. public function event($name, $payload = null) {
  608. $this->log->log("EVENT: $name", XMPPHP_Log::LEVEL_DEBUG);
  609. foreach($this->eventhandlers as $handler) {
  610. if($name == $handler[0]) {
  611. if($handler[2] === null) {
  612. $handler[2] = $this;
  613. }
  614. $handler[2]->$handler[1]($payload);
  615. }
  616. }
  617. if(is_array($this->until))
  618. {
  619. if(in_array($name, $this->until))
  620. {
  621. $this->until_payload = array($name, $payload);
  622. if(!isset($this->until_count))
  623. $this->until_count = 0;
  624. $this->until_count += 1;
  625. }
  626. }
  627. }
  628. /**
  629. * Read from socket
  630. */
  631. public function read() {
  632. $buff = @fread($this->socket, 1024);
  633. if(!$buff) {
  634. if($this->reconnect) {
  635. $this->doReconnect();
  636. } else {
  637. fclose($this->socket);
  638. return false;
  639. }
  640. }
  641. $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
  642. xml_parse($this->parser, $buff, false);
  643. }
  644. /**
  645. * Send to socket
  646. *
  647. * @param string $msg
  648. */
  649. public function send($msg, $timeout=NULL) {
  650. if (is_null($timeout)) {
  651. $secs = NULL;
  652. $usecs = NULL;
  653. } else if ($timeout == 0) {
  654. $secs = 0;
  655. $usecs = 0;
  656. } else {
  657. $maximum = $timeout * 1000000;
  658. $usecs = $maximum % 1000000;
  659. $secs = floor(($maximum - $usecs) / 1000000);
  660. }
  661. $read = array();
  662. $write = array($this->socket);
  663. $except = array();
  664. $select = @stream_select($read, $write, $except, $secs, $usecs);
  665. if($select === False) {
  666. $this->log->log("ERROR sending message; reconnecting.");
  667. $this->doReconnect();
  668. # TODO: retry send here
  669. return false;
  670. } elseif ($select > 0) {
  671. $this->log->log("Socket is ready; send it.", XMPPHP_Log::LEVEL_VERBOSE);
  672. } else {
  673. $this->log->log("Socket is not ready; break.", XMPPHP_Log::LEVEL_ERROR);
  674. return false;
  675. }
  676. $sentbytes = @fwrite($this->socket, $msg);
  677. $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), XMPPHP_Log::LEVEL_VERBOSE);
  678. if($sentbytes === FALSE) {
  679. $this->log->log("ERROR sending message; reconnecting.", XMPPHP_Log::LEVEL_ERROR);
  680. $this->doReconnect();
  681. return false;
  682. }
  683. $this->log->log("Successfully sent $sentbytes bytes.", XMPPHP_Log::LEVEL_VERBOSE);
  684. return $sentbytes;
  685. }
  686. public function time() {
  687. list($usec, $sec) = explode(" ", microtime());
  688. return (float)$sec + (float)$usec;
  689. }
  690. /**
  691. * Reset connection
  692. */
  693. public function reset() {
  694. $this->xml_depth = 0;
  695. unset($this->xmlobj);
  696. $this->xmlobj = array();
  697. $this->setupParser();
  698. if(!$this->is_server) {
  699. $this->send($this->stream_start);
  700. }
  701. $this->been_reset = true;
  702. }
  703. /**
  704. * Setup the XML parser
  705. */
  706. public function setupParser() {
  707. $this->parser = xml_parser_create('UTF-8');
  708. xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
  709. xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
  710. xml_set_object($this->parser, $this);
  711. xml_set_element_handler($this->parser, 'startXML', 'endXML');
  712. xml_set_character_data_handler($this->parser, 'charXML');
  713. }
  714. public function readyToProcess() {
  715. $read = array($this->socket);
  716. $write = array();
  717. $except = array();
  718. $updated = @stream_select($read, $write, $except, 0);
  719. return (($updated !== false) && ($updated > 0));
  720. }
  721. }