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