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