XMLStream.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  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 Alexander Birkner (https://github.com/BirknerAlex)
  27. * @author zorn-v (https://github.com/zorn-v/xmpphp/)
  28. * @author GNU social
  29. * @copyright 2008 Nathanael C. Fritz
  30. */
  31. namespace XMPPHP;
  32. /** Exception */
  33. require_once __DIR__ . DIRECTORY_SEPARATOR . 'Exception.php';
  34. /** XMLObj */
  35. require_once __DIR__ . DIRECTORY_SEPARATOR . 'XMLObj.php';
  36. /** Log */
  37. require_once __DIR__ . DIRECTORY_SEPARATOR . 'Log.php';
  38. /**
  39. * XMPPHP XMLStream
  40. *
  41. * @package XMPPHP
  42. * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
  43. * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
  44. * @author Michael Garvin <JID: gar@netflint.net>
  45. * @copyright 2008 Nathanael C. Fritz
  46. * @version $Id$
  47. */
  48. class XMLStream
  49. {
  50. /**
  51. * @var resource
  52. */
  53. protected $socket;
  54. /**
  55. * @var resource
  56. */
  57. protected $parser;
  58. /**
  59. * @var string
  60. */
  61. protected $buffer;
  62. /**
  63. * @var int
  64. */
  65. protected $xml_depth = 0;
  66. /**
  67. * @var string
  68. */
  69. protected $host;
  70. /**
  71. * @var integer
  72. */
  73. protected $port;
  74. /**
  75. * @var string
  76. */
  77. protected $stream_start = '<stream>';
  78. /**
  79. * @var string
  80. */
  81. protected $stream_end = '</stream>';
  82. /**
  83. * @var bool
  84. */
  85. protected $disconnected = false;
  86. /**
  87. * @var bool
  88. */
  89. protected $sent_disconnect = false;
  90. /**
  91. * @var array
  92. */
  93. protected $ns_map = [];
  94. /**
  95. * @var array
  96. */
  97. protected $current_ns = [];
  98. /**
  99. * @var array
  100. */
  101. protected $xmlobj = null;
  102. /**
  103. * @var array
  104. */
  105. protected $nshandlers = [];
  106. /**
  107. * @var array
  108. */
  109. protected $xpathhandlers = [];
  110. /**
  111. * @var array
  112. */
  113. protected $idhandlers = [];
  114. /**
  115. * @var array
  116. */
  117. protected $eventhandlers = [];
  118. /**
  119. * @var int
  120. */
  121. protected $lastid = 0;
  122. /**
  123. * @var string
  124. */
  125. protected $default_ns;
  126. /**
  127. * @var string[]
  128. */
  129. protected $until = [];
  130. /**
  131. * @var int[]
  132. */
  133. protected $until_count = [];
  134. /**
  135. * @var array
  136. */
  137. protected $until_happened = false;
  138. /**
  139. * @var array
  140. */
  141. protected $until_payload = [];
  142. /**
  143. * @var Log
  144. */
  145. protected $log;
  146. /**
  147. * @var bool
  148. */
  149. protected $reconnect = true;
  150. /**
  151. * @var bool
  152. */
  153. protected $been_reset = false;
  154. /**
  155. * @var bool
  156. */
  157. protected $is_server;
  158. /**
  159. * @var float
  160. */
  161. protected $last_send = 0;
  162. /**
  163. * @var bool
  164. */
  165. protected $use_ssl = false;
  166. /**
  167. * @var int
  168. */
  169. protected $reconnectTimeout = 30;
  170. /**
  171. * Constructor
  172. *
  173. * @param string|null $host (optional)
  174. * @param string|null $port (optional)
  175. * @param bool $print_log (optional)
  176. * @param string $log_level (optional)
  177. * @param bool $is_server (optional)
  178. */
  179. public function __construct(
  180. ?string $host = null,
  181. ?string $port = null,
  182. bool $print_log = false,
  183. ?string $log_level = null,
  184. bool $is_server = false
  185. ) {
  186. $this->reconnect = !$is_server;
  187. $this->is_server = $is_server;
  188. $this->host = $host;
  189. $this->port = $port;
  190. $this->setupParser();
  191. $this->log = new Log($print_log, $log_level);
  192. }
  193. /**
  194. * Setup the XML parser
  195. */
  196. public function setupParser(): void
  197. {
  198. $this->parser = xml_parser_create('UTF-8');
  199. xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
  200. xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
  201. xml_set_object($this->parser, $this);
  202. xml_set_element_handler($this->parser, 'startXML', 'endXML');
  203. xml_set_character_data_handler($this->parser, 'charXML');
  204. }
  205. /**
  206. * Destructor
  207. * Cleanup connection
  208. * @throws Exception
  209. */
  210. public function __destruct()
  211. {
  212. if (!$this->disconnected && $this->socket) {
  213. $this->disconnect();
  214. }
  215. }
  216. /**
  217. * Disconnect from XMPP Host
  218. * @throws Exception
  219. */
  220. public function disconnect(): void
  221. {
  222. $this->log->log("Disconnecting...", Log::LEVEL_VERBOSE);
  223. if (false == (bool)$this->socket) {
  224. return;
  225. }
  226. $this->reconnect = false;
  227. $this->send($this->stream_end);
  228. $this->sent_disconnect = true;
  229. $this->processUntil('end_stream', 5);
  230. $this->disconnected = true;
  231. }
  232. /**
  233. * Send to socket
  234. *
  235. * @param string $msg
  236. * @param int|null $timeout
  237. * @return bool|int
  238. * @throws Exception
  239. */
  240. public function send(string $msg, ?int $timeout = null)
  241. {
  242. if (is_null($timeout)) {
  243. $secs = null;
  244. $usecs = null;
  245. } elseif ($timeout == 0) {
  246. $secs = 0;
  247. $usecs = 0;
  248. } else {
  249. $maximum = $timeout * 1000000;
  250. $usecs = $maximum % 1000000;
  251. $secs = floor(($maximum - $usecs) / 1000000);
  252. }
  253. $read = [];
  254. $write = [$this->socket];
  255. $except = [];
  256. $select = @stream_select($read, $write, $except, $secs, $usecs);
  257. if ($select === false) {
  258. $this->log->log("ERROR sending message; reconnecting.");
  259. $this->doReconnect();
  260. // TODO: retry send here
  261. return false;
  262. } elseif ($select > 0) {
  263. $this->log->log("Socket is ready; send it.", Log::LEVEL_VERBOSE);
  264. } else {
  265. $this->log->log("Socket is not ready; break.", Log::LEVEL_ERROR);
  266. return false;
  267. }
  268. $sentbytes = @fwrite($this->socket, $msg);
  269. $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), Log::LEVEL_VERBOSE);
  270. if ($sentbytes === false) {
  271. $this->log->log("ERROR sending message; reconnecting.", Log::LEVEL_ERROR);
  272. $this->doReconnect();
  273. return false;
  274. }
  275. $this->log->log("Successfully sent $sentbytes bytes.", Log::LEVEL_VERBOSE);
  276. return $sentbytes;
  277. }
  278. /**
  279. * Reconnect XMPP Host
  280. * @throws Exception
  281. */
  282. public function doReconnect()
  283. {
  284. if (!$this->is_server) {
  285. $this->log->log("Reconnecting ($this->reconnectTimeout)...", Log::LEVEL_WARNING);
  286. $this->connect(false, false, $this->reconnectTimeout);
  287. $this->reset();
  288. $this->event('reconnect');
  289. }
  290. }
  291. /**
  292. * Connect to XMPP Host
  293. *
  294. * @param bool $persistent (optional)
  295. * @param bool $send_init (optional)
  296. * @param int $timeout (optional)
  297. * @throws Exception
  298. */
  299. public function connect(bool $persistent = false, bool $send_init = true, int $timeout = 30): void
  300. {
  301. $this->sent_disconnect = false;
  302. $start_time = time();
  303. do {
  304. $this->disconnected = false;
  305. $this->sent_disconnect = false;
  306. if ($persistent) {
  307. $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
  308. } else {
  309. $conflag = STREAM_CLIENT_CONNECT;
  310. }
  311. $conn_type = 'tcp';
  312. if ($this->use_ssl) {
  313. $conn_type = 'ssl';
  314. }
  315. $this->log->log("Connecting to $conn_type://{$this->host}:{$this->port}");
  316. $this->socket = @stream_socket_client("$conn_type://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag);
  317. if (!$this->socket) {
  318. $this->log->log("Could not connect.", Log::LEVEL_ERROR);
  319. $this->disconnected = true;
  320. # Take it easy for a few seconds
  321. sleep(min($timeout, 5));
  322. }
  323. } while (!$this->socket && (time() - $start_time) < $timeout);
  324. if ($this->socket) {
  325. stream_set_blocking($this->socket, 1);
  326. if ($send_init) {
  327. $this->send($this->stream_start);
  328. }
  329. } else {
  330. throw new Exception("Could not connect before timeout.");
  331. }
  332. }
  333. /**
  334. * Reset connection
  335. * @throws Exception
  336. */
  337. public function reset(): void
  338. {
  339. $this->xml_depth = 0;
  340. unset($this->xmlobj);
  341. $this->xmlobj = [];
  342. $this->setupParser();
  343. if (!$this->is_server) {
  344. $this->send($this->stream_start);
  345. }
  346. $this->been_reset = true;
  347. }
  348. /**
  349. * Event?
  350. *
  351. * @param string $name
  352. * @param array|null $payload
  353. */
  354. public function event(string $name, ?array $payload = null): void
  355. {
  356. $this->log->log("EVENT: {$name}", Log::LEVEL_DEBUG);
  357. foreach ($this->eventhandlers as $handler) {
  358. if ($handler[0] === $name) {
  359. call_user_func_array($handler[1], [&$payload]);
  360. }
  361. }
  362. foreach ($this->until as $key => $until) {
  363. if (is_array($until)) {
  364. if (in_array($name, $until)) {
  365. $this->until_payload[$key][] = [$name, $payload];
  366. if (!isset($this->until_count[$key])) {
  367. $this->until_count[$key] = 0;
  368. }
  369. $this->until_count[$key] += 1;
  370. //$this->until[$key] = false;
  371. }
  372. }
  373. }
  374. }
  375. /**
  376. * Process until a specified event or a timeout occurs
  377. *
  378. * @param string|array $event
  379. * @param int $timeout (optional)
  380. * @return array
  381. * @throws Exception
  382. */
  383. public function processUntil($event, int $timeout = -1): array
  384. {
  385. $start = time();
  386. if (!is_array($event)) {
  387. $event = array($event);
  388. }
  389. $this->until[] = $event;
  390. end($this->until);
  391. $event_key = key($this->until);
  392. reset($this->until);
  393. $this->until_count[$event_key] = 0;
  394. while (!$this->disconnected and $this->until_count[$event_key] < 1 and (time() - $start < $timeout or $timeout == -1)) {
  395. $this->__process();
  396. }
  397. if (array_key_exists($event_key, $this->until_payload)) {
  398. $payload = $this->until_payload[$event_key];
  399. unset($this->until_payload[$event_key]);
  400. unset($this->until_count[$event_key]);
  401. unset($this->until[$event_key]);
  402. } else {
  403. $payload = [];
  404. }
  405. return $payload;
  406. }
  407. /**
  408. * Core reading tool
  409. * 0 -> only read if data is immediately ready
  410. * NULL -> wait forever and ever
  411. * integer -> process for this amount of time
  412. * @param int $maximum
  413. * @return bool
  414. * @throws Exception
  415. */
  416. private function __process(int $maximum = 5): bool
  417. {
  418. $remaining = $maximum;
  419. do {
  420. $starttime = (microtime(true) * 1000000);
  421. $read = array($this->socket);
  422. $write = [];
  423. $except = [];
  424. if (is_null($maximum)) {
  425. $secs = null;
  426. $usecs = null;
  427. } elseif ($maximum == 0) {
  428. $secs = 0;
  429. $usecs = 0;
  430. } else {
  431. $usecs = $remaining % 1000000;
  432. $secs = floor(($remaining - $usecs) / 1000000);
  433. }
  434. $updated = @stream_select($read, $write, $except, $secs, $usecs);
  435. if ($updated === false) {
  436. $this->log->log("Error on stream_select()", Log::LEVEL_VERBOSE);
  437. if ($this->reconnect) {
  438. $this->doReconnect();
  439. } else {
  440. fclose($this->socket);
  441. $this->socket = null;
  442. return false;
  443. }
  444. } elseif ($updated > 0) {
  445. # XXX: Is this big enough?
  446. $buff = @fread($this->socket, 4096);
  447. if (!$buff) {
  448. if ($this->reconnect) {
  449. $this->doReconnect();
  450. } else {
  451. fclose($this->socket);
  452. $this->socket = null;
  453. return false;
  454. }
  455. }
  456. $this->log->log("RECV: $buff", Log::LEVEL_VERBOSE);
  457. xml_parse($this->parser, $buff, false);
  458. } // Otherwise,
  459. // $updated == 0 means no changes during timeout.
  460. $endtime = (microtime(true) * 1000000);
  461. $time_past = $endtime - $starttime;
  462. $remaining = $remaining - $time_past;
  463. } while (is_null($maximum) || $remaining > 0);
  464. return true;
  465. }
  466. /**
  467. * Return the log instance
  468. *
  469. * @return Log
  470. */
  471. public function getLog(): Log
  472. {
  473. return $this->log;
  474. }
  475. /**
  476. * Get next ID
  477. *
  478. * @return string
  479. */
  480. public function getId(): string
  481. {
  482. ++$this->lastid;
  483. return (string) $this->lastid;
  484. }
  485. /**
  486. * Set SSL
  487. * @param bool $use
  488. */
  489. public function useSSL(bool $use = true): void
  490. {
  491. $this->use_ssl = $use;
  492. }
  493. /**
  494. * Compose a proper callable if given legacy syntax
  495. *
  496. * @param callable|string $pointer
  497. * @param object|null|bool $obj
  498. * @return callable
  499. * @throws InvalidArgumentException
  500. */
  501. protected function ensureHandler($pointer, $obj = false): callable
  502. {
  503. $handler = $pointer;
  504. if (is_string($pointer)) {
  505. if (is_object($obj)) {
  506. $handler = [$obj, $pointer];
  507. } elseif (is_null($obj)) {
  508. // Questionable behaviour for backwards compatibility
  509. $handler = [$this, $pointer];
  510. }
  511. }
  512. if (!is_callable($handler)) {
  513. throw new \InvalidArgumentException(
  514. 'Cannot compose a proper callable'
  515. );
  516. }
  517. return $handler;
  518. }
  519. /**
  520. * Add ID Handler
  521. *
  522. * @param int $id
  523. * @param callable|string $pointer
  524. * @param object|bool|null $obj
  525. */
  526. public function addIdHandler(string $id, $pointer, $obj = null): void
  527. {
  528. $this->idhandlers[$id] = [$this->ensureHandler($pointer, $obj)];
  529. }
  530. /**
  531. * Add Handler
  532. *
  533. * @param string $name
  534. * @param string $ns
  535. * @param string $pointer
  536. * @param object|bool|null $obj
  537. * @param int $depth
  538. *
  539. * public function addHandler(string $name, string $ns, $pointer, $obj = null, int $depth = 1): void
  540. * {
  541. * // TODO deprecation warning
  542. * $this->nshandlers[] = [$name, $ns, $this->ensureHandler($pointer, $obj), $depth];
  543. * }*/
  544. /**
  545. * Add XPath Handler
  546. *
  547. * @param string $xpath
  548. * @param callable|string $pointer
  549. * @param object|bool|null $obj
  550. */
  551. public function addXPathHandler(string $xpath, $pointer, $obj = null): void
  552. {
  553. if (preg_match_all('/\/?(\{[^\}]+\})?[^\/]+/', $xpath, $regs)) {
  554. $tag = $regs[0];
  555. } else {
  556. $tag = [$xpath];
  557. }
  558. $xpath_array = [];
  559. foreach ($tag as $t) {
  560. $t = ltrim($t, '/');
  561. preg_match('/(\{([^\}]+)\})?(.*)/', $t, $regs);
  562. $xpath_array[] = [$regs[2], $regs[3]];
  563. }
  564. $this->xpathhandlers[] = [$xpath_array, $this->ensureHandler($pointer, $obj)];
  565. }
  566. /**
  567. * Add Event Handler
  568. *
  569. * @param string $name
  570. * @param callable|string $pointer
  571. * @param object|bool|null $obj
  572. */
  573. public function addEventHandler(string $name, $pointer, $obj = null): void
  574. {
  575. $this->eventhandlers[] = [$name, $this->ensureHandler($pointer, $obj)];
  576. }
  577. /**
  578. * @param int $timeout
  579. */
  580. public function setReconnectTimeout(int $timeout): void
  581. {
  582. $this->reconnectTimeout = $timeout;
  583. }
  584. /**
  585. * Are we are disconnected?
  586. *
  587. * @return bool
  588. */
  589. public function isDisconnected(): bool
  590. {
  591. return $this->disconnected;
  592. }
  593. /**
  594. * Process
  595. *
  596. * @throws Exception
  597. */
  598. public function process(): void
  599. {
  600. $this->__process(null);
  601. }
  602. /**
  603. * Process until a timeout occurs
  604. *
  605. * @param integer $timeout
  606. * @return string
  607. * @throws Exception
  608. */
  609. public function processTime($timeout = null): string
  610. {
  611. if (is_null($timeout)) {
  612. return $this->__process(null);
  613. } else {
  614. return $this->__process($timeout * 1000000);
  615. }
  616. }
  617. /**
  618. * Obsolete?
  619. * @param $socket
  620. *
  621. * public function Xapply_socket($socket)
  622. * {
  623. * $this->socket = $socket;
  624. * }*/
  625. /**
  626. * XML start callback
  627. *
  628. * @param resource $parser
  629. * @param string $name
  630. * @param array $attr
  631. * @see xml_set_element_handler
  632. */
  633. public function startXML($parser, string $name, array $attr): void
  634. {
  635. if ($this->been_reset) {
  636. $this->been_reset = false;
  637. $this->xml_depth = 0;
  638. }
  639. $this->xml_depth++;
  640. if (array_key_exists('XMLNS', $attr)) {
  641. $this->current_ns[$this->xml_depth] = $attr['XMLNS'];
  642. } else {
  643. $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1];
  644. if (!$this->current_ns[$this->xml_depth]) {
  645. $this->current_ns[$this->xml_depth] = $this->default_ns;
  646. }
  647. }
  648. $ns = $this->current_ns[$this->xml_depth];
  649. foreach ($attr as $key => $value) {
  650. if (strstr($key, ":")) {
  651. $key = explode(':', $key);
  652. $key = $key[1];
  653. $this->ns_map[$key] = $value;
  654. }
  655. }
  656. if (!strstr($name, ":") === false) {
  657. $name = explode(':', $name);
  658. $ns = $this->ns_map[$name[0]];
  659. $name = $name[1];
  660. }
  661. $obj = new XMLObj($name, $ns, $attr);
  662. if ($this->xml_depth > 1) {
  663. $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj;
  664. }
  665. $this->xmlobj[$this->xml_depth] = $obj;
  666. }
  667. /**
  668. * XML end callback
  669. *
  670. * @param resource $parser
  671. * @param string $name
  672. * @throws Exception
  673. * @see xml_set_element_handler
  674. *
  675. */
  676. public function endXML($parser, string $name): void
  677. {
  678. #$this->log->log("Ending $name", Log::LEVEL_DEBUG);
  679. #print "$name\n";
  680. if ($this->been_reset) {
  681. $this->been_reset = false;
  682. $this->xml_depth = 0;
  683. }
  684. $this->xml_depth--;
  685. if ($this->xml_depth == 1) {
  686. #clean-up old objects
  687. #$found = false; #FIXME This didn't appear to be in use --Gar
  688. $searchxml = null;
  689. foreach ($this->xpathhandlers as $handler) {
  690. if (is_array($this->xmlobj) && array_key_exists(2, $this->xmlobj)) {
  691. $searchxml = $this->xmlobj[2];
  692. $nstag = array_shift($handler[0]);
  693. if (($nstag[0] == null or $searchxml->ns == $nstag[0]) and ($nstag[1] == "*" or $nstag[1] == $searchxml->name)) {
  694. foreach ($handler[0] as $nstag) {
  695. if ($searchxml !== null and $searchxml->hasSub($nstag[1], $ns = $nstag[0])) {
  696. $searchxml = $searchxml->sub($nstag[1], $ns = $nstag[0]);
  697. } else {
  698. $searchxml = null;
  699. break;
  700. }
  701. }
  702. if (!is_null($searchxml)) {
  703. call_user_func_array($handler[1], [&$this->xmlobj[2]]);
  704. }
  705. }
  706. }
  707. }
  708. foreach ($this->nshandlers as $handler) {
  709. if ($handler[4] != 1 and array_key_exists(2, $this->xmlobj) and $this->xmlobj[2]->hasSub($handler[0])) {
  710. $searchxml = $this->xmlobj[2]->sub($handler[0]);
  711. } elseif (is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) {
  712. $searchxml = $this->xmlobj[2];
  713. }
  714. if (
  715. !is_null($searchxml)
  716. && $searchxml->name === $handler[0]
  717. && (
  718. (!$handler[1] && $searchxml->ns === $this->default_ns)
  719. || $searchxml->ns === $handler[1]
  720. )
  721. ) {
  722. call_user_func_array($handler[2], [&$this->xmlobj[2]]);
  723. }
  724. }
  725. foreach ($this->idhandlers as $id => $handler) {
  726. if (
  727. array_key_exists(2, $this->xmlobj)
  728. && array_key_exists('id', $this->xmlobj[2]->attrs)
  729. && $this->xmlobj[2]->attrs['id'] === (string) $id
  730. ) {
  731. call_user_func_array($handler[0], [&$this->xmlobj[2]]);
  732. // id handlers are only used once
  733. unset($this->idhandlers[$id]);
  734. break;
  735. }
  736. }
  737. if (is_array($this->xmlobj)) {
  738. $this->xmlobj = array_slice($this->xmlobj, 0, 1);
  739. if (isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMLObj) {
  740. $this->xmlobj[0]->subs = null;
  741. }
  742. }
  743. unset($this->xmlobj[2]);
  744. }
  745. if ($this->xml_depth == 0 and !$this->been_reset) {
  746. if (!$this->disconnected) {
  747. if (!$this->sent_disconnect) {
  748. $this->send($this->stream_end);
  749. }
  750. $this->disconnected = true;
  751. $this->sent_disconnect = true;
  752. fclose($this->socket);
  753. if ($this->reconnect) {
  754. $this->doReconnect();
  755. }
  756. }
  757. $this->event('end_stream');
  758. }
  759. }
  760. /**
  761. * XML character callback
  762. * @param resource $parser
  763. * @param string $data
  764. * @see xml_set_character_data_handler
  765. *
  766. */
  767. public function charXML($parser, string $data): void
  768. {
  769. if (array_key_exists($this->xml_depth, $this->xmlobj)) {
  770. $this->xmlobj[$this->xml_depth]->data .= $data;
  771. }
  772. }
  773. /**
  774. * Read from socket
  775. * @return bool Did read
  776. * @throws Exception
  777. */
  778. public function read(): bool
  779. {
  780. $buff = @fread($this->socket, 1024);
  781. if (!$buff) {
  782. if ($this->reconnect) {
  783. $this->doReconnect();
  784. } else {
  785. fclose($this->socket);
  786. return false;
  787. }
  788. }
  789. $this->log->log("RECV: $buff", Log::LEVEL_VERBOSE);
  790. xml_parse($this->parser, $buff, false);
  791. return true;
  792. }
  793. public function time(): float
  794. {
  795. list($usec, $sec) = explode(" ", microtime());
  796. return (float)$sec + (float)$usec;
  797. }
  798. public function readyToProcess(): bool
  799. {
  800. $read = array($this->socket);
  801. $write = [];
  802. $except = [];
  803. $updated = @stream_select($read, $write, $except, 0);
  804. return (($updated !== false) && ($updated > 0));
  805. }
  806. }