3 * XMPPHP: The PHP XMPP Library
4 * Copyright (C) 2008 Nathanael C. Fritz
5 * This file is part of SleekXMPP.
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.
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.
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
23 * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
24 * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
25 * @copyright 2008 Nathanael C. Fritz
28 /** XMPPHP_Exception */
29 require_once 'Exception.php';
32 require_once 'XMLObj.php';
35 require_once 'Log.php';
42 * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
43 * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
44 * @copyright 2008 Nathanael C. Fritz
47 class XMPPHP_XMLStream {
63 protected $xml_depth = 0;
75 protected $stream_start = '<stream>';
79 protected $stream_end = '</stream>';
83 protected $disconnected = false;
87 protected $sent_disconnect = false;
91 protected $ns_map = array();
95 protected $current_ns = array();
99 protected $xmlobj = null;
103 protected $nshandlers = array();
107 protected $idhandlers = array();
111 protected $eventhandlers = array();
115 protected $lastid = 0;
119 protected $default_ns;
123 protected $until = '';
127 protected $until_happened = false;
131 protected $until_payload = array();
139 protected $reconnect = true;
143 protected $been_reset = false;
147 protected $is_server;
151 protected $last_send = 0;
155 protected $use_ssl = false;
160 * @param string $host
161 * @param string $port
162 * @param boolean $printlog
163 * @param string $loglevel
164 * @param boolean $is_server
166 public function __construct($host = null, $port = null, $printlog = false, $loglevel = null, $is_server = false) {
167 $this->reconnect = !$is_server;
168 $this->is_server = $is_server;
171 $this->setupParser();
172 $this->log = new XMPPHP_Log($printlog, $loglevel);
179 public function __destruct() {
180 if(!$this->disconnected && $this->socket) {
186 * Return the log instance
190 public function getLog() {
199 public function getId() {
201 return $this->lastid;
209 public function useSSL($use=true) {
210 $this->use_ssl = $use;
217 * @param string $pointer
220 public function addIdHandler($id, $pointer, $obj = null) {
221 $this->idhandlers[$id] = array($pointer, $obj);
229 * @param string $pointer
231 * @param integer $depth
233 public function addHandler($name, $ns, $pointer, $obj = null, $depth = 1) {
234 $this->nshandlers[] = array($name,$ns,$pointer,$obj, $depth);
241 * @param string $pointer
244 public function addEventHandler($name, $pointer, $obj) {
245 $this->eventhandlers[] = array($name, $pointer, $obj);
249 * Connect to XMPP Host
251 * @param integer $timeout
252 * @param boolean $persistent
253 * @param boolean $sendinit
255 public function connect($timeout = 30, $persistent = false, $sendinit = true) {
256 $this->disconnected = false;
257 $this->sent_disconnect = false;
259 $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
261 $conflag = STREAM_CLIENT_CONNECT;
264 if($this->use_ssl) $conntype = 'ssl';
265 $this->log->log("Connecting to $conntype://{$this->host}:{$this->port}");
267 $this->socket = @stream_socket_client("$conntype://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag);
268 } catch (Exception $e) {
269 throw new XMPPHP_Exception($e->getMessage());
272 $this->log->log("Could not connect.", XMPPHP_Log::LEVEL_ERROR);
273 $this->disconnected = true;
275 throw new XMPPHP_Exception('Could not connect.');
277 stream_set_blocking($this->socket, 1);
278 if($sendinit) $this->send($this->stream_start);
282 * Reconnect XMPP Host
284 public function doReconnect() {
285 if(!$this->is_server) {
286 $this->log->log("Reconnecting...", XMPPHP_Log::LEVEL_WARNING);
287 $this->connect(30, false, false);
293 * Disconnect from XMPP Host
295 public function disconnect() {
296 $this->log->log("Disconnecting...", XMPPHP_Log::LEVEL_VERBOSE);
297 $this->reconnect = false;
298 $this->send($this->stream_end);
299 $this->sent_disconnect = true;
300 $this->processUntil('end_stream', 5);
301 $this->disconnected = true;
305 * Are we are disconnected?
309 public function isDisconnected() {
310 return $this->disconnected;
315 * 0 -> only read if data is immediately ready
316 * NULL -> wait forever and ever
317 * integer -> process for this amount of time
320 private function __process($maximum=0) {
322 $this->log->log("__process($maximum)", XMPPHP_Log::LEVEL_VERBOSE);
324 $remaining = $maximum;
327 $starttime = microtime();
328 $read = array($this->socket);
331 if (is_null($maximum)) {
334 } else if ($maximum == 0) {
338 $secs = $remaining / 1000000;
339 $usecs = $remaining % 1000000;
341 $this->log->log("stream_select(read, write, except, $secs, $usecs)", XMPPHP_Log::LEVEL_VERBOSE);
342 $updated = @stream_select($read, $write, $except, $secs, $usecs);
343 if ($updated === false) {
344 $this->log->log("Error on stream_select()", XMPPHP_Log::LEVEL_VERBOSE);
345 if ($this->reconnect) {
346 $this->doReconnect();
348 $this->log->log("Giving up", XMPPHP_Log::LEVEL_VERBOSE);
349 fclose($this->socket);
352 } else if ($updated > 0) {
353 # XXX: Is this big enough?
354 $this->log->log("Reading from socket", XMPPHP_Log::LEVEL_VERBOSE);
355 $buff = @fread($this->socket, 4096);
357 if($this->reconnect) {
358 $this->doReconnect();
360 $this->log->log("Error on fread(), reconnect", XMPPHP_Log::LEVEL_VERBOSE);
361 fclose($this->socket);
365 $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
366 xml_parse($this->parser, $buff, false);
368 # $updated == 0 means no changes during timeout.
370 $remaining -= (microtime() - $starttime);
371 } while (is_null($maximum) || $remaining > 0);
380 public function process() {
381 $this->__process(NULL);
385 * Process until a timeout occurs
387 * @param integer $timeout
390 public function processTime($timeout=NULL) {
391 if (is_null($timeout)) {
392 return $this->__process(NULL);
394 return $this->__process($timeout * 1000000);
399 * Process until a specified event or a timeout occurs
401 * @param string|array $event
402 * @param integer $timeout
405 public function processUntil($event, $timeout=-1) {
407 if(!is_array($event)) $event = array($event);
408 $this->until[] = $event;
410 $event_key = key($this->until);
413 while(!$this->disconnected and $this->until[$event_key] and (time() - $start < $timeout or $timeout == -1)) {
416 if(array_key_exists($event_key, $this->until_payload)) {
417 $payload = $this->until_payload[$event_key];
421 unset($this->until_payload[$event_key]);
428 public function Xapply_socket($socket) {
429 $this->socket = $socket;
435 * @see xml_set_element_handler
437 * @param resource $parser
438 * @param string $name
440 public function startXML($parser, $name, $attr) {
441 if($this->been_reset) {
442 $this->been_reset = false;
443 $this->xml_depth = 0;
446 if(array_key_exists('XMLNS', $attr)) {
447 $this->current_ns[$this->xml_depth] = $attr['XMLNS'];
449 $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1];
450 if(!$this->current_ns[$this->xml_depth]) $this->current_ns[$this->xml_depth] = $this->default_ns;
452 $ns = $this->current_ns[$this->xml_depth];
453 foreach($attr as $key => $value) {
454 if(strstr($key, ":")) {
455 $key = explode(':', $key);
457 $this->ns_map[$key] = $value;
460 if(!strstr($name, ":") === false)
462 $name = explode(':', $name);
463 $ns = $this->ns_map[$name[0]];
466 $obj = new XMPPHP_XMLObj($name, $ns, $attr);
467 if($this->xml_depth > 1) {
468 $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj;
470 $this->xmlobj[$this->xml_depth] = $obj;
476 * @see xml_set_element_handler
478 * @param resource $parser
479 * @param string $name
481 public function endXML($parser, $name) {
482 #$this->log->log("Ending $name", XMPPHP_Log::LEVEL_DEBUG);
484 if($this->been_reset) {
485 $this->been_reset = false;
486 $this->xml_depth = 0;
489 if($this->xml_depth == 1) {
490 #clean-up old objects
492 foreach($this->nshandlers as $handler) {
493 if($handler[4] != 1 and $this->xmlobj[2]->hasSub($handler[0])) {
494 $searchxml = $this->xmlobj[2]->sub($handler[0]);
495 } elseif(is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) {
496 $searchxml = $this->xmlobj[2];
498 if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) {
499 if($handler[3] === null) $handler[3] = $this;
500 $this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG);
501 $handler[3]->$handler[2]($this->xmlobj[2]);
504 foreach($this->idhandlers as $id => $handler) {
505 if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) {
506 if($handler[1] === null) $handler[1] = $this;
507 $handler[1]->$handler[0]($this->xmlobj[2]);
508 #id handlers are only used once
509 unset($this->idhandlers[$id]);
513 if(is_array($this->xmlobj)) {
514 $this->xmlobj = array_slice($this->xmlobj, 0, 1);
515 if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMPPHP_XMLObj) {
516 $this->xmlobj[0]->subs = null;
519 unset($this->xmlobj[2]);
521 if($this->xml_depth == 0 and !$this->been_reset) {
522 if(!$this->disconnected) {
523 if(!$this->sent_disconnect) {
524 $this->send($this->stream_end);
526 $this->disconnected = true;
527 $this->sent_disconnect = true;
528 fclose($this->socket);
529 if($this->reconnect) {
530 $this->doReconnect();
533 $this->event('end_stream');
538 * XML character callback
539 * @see xml_set_character_data_handler
541 * @param resource $parser
542 * @param string $data
544 public function charXML($parser, $data) {
545 if(array_key_exists($this->xml_depth, $this->xmlobj)) {
546 $this->xmlobj[$this->xml_depth]->data .= $data;
553 * @param string $name
554 * @param string $payload
556 public function event($name, $payload = null) {
557 $this->log->log("EVENT: $name", XMPPHP_Log::LEVEL_DEBUG);
558 foreach($this->eventhandlers as $handler) {
559 if($name == $handler[0]) {
560 if($handler[2] === null) {
563 $handler[2]->$handler[1]($payload);
566 foreach($this->until as $key => $until) {
567 if(is_array($until)) {
568 if(in_array($name, $until)) {
569 $this->until_payload[$key][] = array($name, $payload);
570 $this->until[$key] = false;
579 public function read() {
580 $buff = @fread($this->socket, 1024);
582 if($this->reconnect) {
583 $this->doReconnect();
585 fclose($this->socket);
589 $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
590 xml_parse($this->parser, $buff, false);
598 public function send($msg, $rec=false) {
599 if($this->time() - $this->last_send < .1) {
605 $write = array($this->socket);
607 $select = @stream_select($read, $write, $except, 0, 0);
608 if($select === False) {
609 $this->doReconnect();
611 } elseif ($select > 0) {
615 //$this->processTime(.25);
618 $sentbytes = @fwrite($this->socket, $msg, 1024);
619 $this->last_send = $this->time();
620 $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), XMPPHP_Log::LEVEL_VERBOSE);
621 if($sentbytes === FALSE) {
622 $this->doReconnect();
623 } elseif ($sentbytes != mb_strlen($msg, '8bit')) {
624 $this->send(mb_substr($msg, $sentbytes, mb_strlen($msg, '8bit'), '8bit'), true);
628 public function time() {
629 list($usec, $sec) = explode(" ", microtime());
630 return (float)$sec + (float)$usec;
636 public function reset() {
637 $this->xml_depth = 0;
638 unset($this->xmlobj);
639 $this->xmlobj = array();
640 $this->setupParser();
641 if(!$this->is_server) {
642 $this->send($this->stream_start);
644 $this->been_reset = true;
648 * Setup the XML parser
650 public function setupParser() {
651 $this->parser = xml_parser_create('UTF-8');
652 xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
653 xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
654 xml_set_object($this->parser, $this);
655 xml_set_element_handler($this->parser, 'startXML', 'endXML');
656 xml_set_character_data_handler($this->parser, 'charXML');
659 public function readyToProcess() {
660 $read = array($this->socket);
663 $updated = @stream_select($read, $write, $except, 0);
664 return (($updated !== false) && ($updated > 0));