MDL-16500 Removed the old Jabber class from the module and moved to a new one called...
[moodle.git] / lib / jabber / XMPP / XMLStream.php
CommitLineData
838a8eb1 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 */
29require_once 'Exception.php';
30
31/** XMPPHP_XMLObj */
32require_once 'XMLObj.php';
33
34/** XMPPHP_Log */
35require_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 */
47class 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 /**
154 * Constructor
155 *
156 * @param string $host
157 * @param string $port
158 * @param boolean $printlog
159 * @param string $loglevel
160 * @param boolean $is_server
161 */
162 public function __construct($host = null, $port = null, $printlog = false, $loglevel = null, $is_server = false) {
163 $this->reconnect = !$is_server;
164 $this->is_server = $is_server;
165 $this->host = $host;
166 $this->port = $port;
167 $this->setupParser();
168 $this->log = new XMPPHP_Log($printlog, $loglevel);
169 }
170
171 /**
172 * Destructor
173 * Cleanup connection
174 */
175 public function __destruct() {
176 if(!$this->disconnected && $this->socket) {
177 $this->disconnect();
178 }
179 }
180
181 /**
182 * Return the log instance
183 *
184 * @return XMPPHP_Log
185 */
186 public function getLog() {
187 return $this->log;
188 }
189
190 /**
191 * Get next ID
192 *
193 * @return integer
194 */
195 public function getId() {
196 $this->lastid++;
197 return $this->lastid;
198 }
199
200 /**
201 * Add ID Handler
202 *
203 * @param integer $id
204 * @param string $pointer
205 * @param string $obj
206 */
207 public function addIdHandler($id, $pointer, $obj = null) {
208 $this->idhandlers[$id] = array($pointer, $obj);
209 }
210
211 /**
212 * Add Handler
213 *
214 * @param integer $id
215 * @param string $ns
216 * @param string $pointer
217 * @param string $obj
218 * @param integer $depth
219 */
220 public function addHandler($name, $ns, $pointer, $obj = null, $depth = 1) {
221 $this->nshandlers[] = array($name,$ns,$pointer,$obj, $depth);
222 }
223
224 /**
225 * Add Evemt Handler
226 *
227 * @param integer $id
228 * @param string $pointer
229 * @param string $obj
230 */
231 public function addEventHandler($name, $pointer, $obj) {
232 $this->eventhanders[] = array($name, $pointer, $obj);
233 }
234
235 /**
236 * Connect to XMPP Host
237 *
238 * @param integer $timeout
239 * @param boolean $persistent
240 * @param boolean $sendinit
241 */
242 public function connect($timeout = 30, $persistent = false, $sendinit = true) {
243 $this->disconnected = false;
244 $this->sent_disconnect = false;
245 if($persistent) {
246 $conflag = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
247 } else {
248 $conflag = STREAM_CLIENT_CONNECT;
249 }
250 $this->log->log("Connecting to tcp://{$this->host}:{$this->port}");
251 try {
252 $this->socket = @stream_socket_client("tcp://{$this->host}:{$this->port}", $errno, $errstr, $timeout, $conflag);
253 } catch (Exception $e) {
254 throw new XMPPHP_Exception($e->getMessage());
255 }
256 if(!$this->socket) {
257 $this->log->log("Could not connect.", XMPPHP_Log::LEVEL_ERROR);
258 $this->disconnected = true;
259
260 throw new XMPPHP_Exception('Could not connect.');
261 }
262 stream_set_blocking($this->socket, 1);
263 if($sendinit) $this->send($this->stream_start);
264 }
265
266 /**
267 * Reconnect XMPP Host
268 */
269 public function doReconnect() {
270 if(!$this->is_server) {
271 $this->log->log("Reconnecting...", XMPPHP_Log::LEVEL_WARNING);
272 $this->connect(30, false, false);
273 $this->reset();
274 }
275 }
276
277 /**
278 * Disconnect from XMPP Host
279 */
280 public function disconnect() {
281 $this->log->log("Disconnecting...", XMPPHP_Log::LEVEL_VERBOSE);
282 $this->reconnect = false;
283 $this->send($this->stream_end);
284 $this->sent_disconnect = true;
285 $this->processUntil('end_stream', 5);
286 $this->disconnected = true;
287 }
288
289 /**
290 * Are we are disconnected?
291 *
292 * @return boolean
293 */
294 public function isDisconnected() {
295 return $this->disconnected;
296 }
297
298 private function __process() {
299 $read = array($this->socket);
300 $write = null;
301 $except = null;
302 $updated = @stream_select($read, $write, $except, 1);
303 if ($updated > 0) {
304 $buff = @fread($this->socket, 1024);
305 if(!$buff) {
306 if($this->reconnect) {
307 $this->doReconnect();
308 } else {
309 fclose($this->socket);
310 return false;
311 }
312 }
313 $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
314 xml_parse($this->parser, $buff, false);
315 }
316 }
317
318 /**
319 * Process
320 *
321 * @return string
322 */
323 public function process() {
324 $updated = '';
325 while(!$this->disconnect) {
326 $this->__process();
327 }
328 }
329
330 /**
331 * Process until a timeout occurs
332 *
333 * @param integer $timeout
334 * @return string
335 */
336 public function processTime($timeout = -1) {
337 $start = time();
338 $updated = '';
339 while(!$this->disconnected and ($timeout == -1 or time() - $start < $timeout)) {
340 $this->__process();
341 }
342 }
343
344 /**
345 * Process until a specified event or a timeout occurs
346 *
347 * @param string|array $event
348 * @param integer $timeout
349 * @return string
350 */
351 public function processUntil($event, $timeout=-1) {
352 $start = time();
353 if(!is_array($event)) $event = array($event);
354 $this->until[] = $event;
355 end($this->until);
356 $event_key = key($this->until);
357 reset($this->until);
358 $updated = '';
359 while(!$this->disconnected and $this->until[$event_key] and (time() - $start < $timeout or $timeout == -1)) {
360 $this->__process();
361 }
362 if(array_key_exists($event_key, $this->until_payload)) {
363 $payload = $this->until_payload[$event_key];
364 } else {
365 $payload = array();
366 }
367 unset($this->until_payload[$event_key]);
368 return $payload;
369 }
370
371 /**
372 * Obsolete?
373 */
374 public function Xapply_socket($socket) {
375 $this->socket = $socket;
376 }
377
378 /**
379 * XML start callback
380 *
381 * @see xml_set_element_handler
382 *
383 * @param resource $parser
384 * @param string $name
385 */
386 public function startXML($parser, $name, $attr) {
387 if($this->been_reset) {
388 $this->been_reset = false;
389 $this->xml_depth = 0;
390 }
391 $this->xml_depth++;
392 if(array_key_exists('XMLNS', $attr)) {
393 $this->current_ns[$this->xml_depth] = $attr['XMLNS'];
394 } else {
395 $this->current_ns[$this->xml_depth] = $this->current_ns[$this->xml_depth - 1];
396 if(!$this->current_ns[$this->xml_depth]) $this->current_ns[$this->xml_depth] = $this->default_ns;
397 }
398 $ns = $this->current_ns[$this->xml_depth];
399 foreach($attr as $key => $value) {
400 if(strstr($key, ":")) {
401 $key = explode(':', $key);
402 $key = $key[1];
403 $this->ns_map[$key] = $value;
404 }
405 }
406 if(!strstr($name, ":") === false)
407 {
408 $name = explode(':', $name);
409 $ns = $this->ns_map[$name[0]];
410 $name = $name[1];
411 }
412 $obj = new XMPPHP_XMLObj($name, $ns, $attr);
413 if($this->xml_depth > 1) {
414 $this->xmlobj[$this->xml_depth - 1]->subs[] = $obj;
415 }
416 $this->xmlobj[$this->xml_depth] = $obj;
417 }
418
419 /**
420 * XML end callback
421 *
422 * @see xml_set_element_handler
423 *
424 * @param resource $parser
425 * @param string $name
426 */
427 public function endXML($parser, $name) {
428 #$this->log->log("Ending $name", XMPPHP_Log::LEVEL_DEBUG);
429 #print "$name\n";
430 if($this->been_reset) {
431 $this->been_reset = false;
432 $this->xml_depth = 0;
433 }
434 $this->xml_depth--;
435 if($this->xml_depth == 1) {
436 #clean-up old objects
437 $found = false;
438 foreach($this->nshandlers as $handler) {
439 if($handler[4] != 1 and $this->xmlobj[2]->hasSub($handler[0])) {
440 $searchxml = $this->xmlobj[2]->sub($handler[0]);
441 } elseif(is_array($this->xmlobj) and array_key_exists(2, $this->xmlobj)) {
442 $searchxml = $this->xmlobj[2];
443 }
444 if($searchxml !== null and $searchxml->name == $handler[0] and ($searchxml->ns == $handler[1] or (!$handler[1] and $searchxml->ns == $this->default_ns))) {
445 if($handler[3] === null) $handler[3] = $this;
446 $this->log->log("Calling {$handler[2]}", XMPPHP_Log::LEVEL_DEBUG);
447 $handler[3]->$handler[2]($this->xmlobj[2]);
448 }
449 }
450 foreach($this->idhandlers as $id => $handler) {
451 if(array_key_exists('id', $this->xmlobj[2]->attrs) and $this->xmlobj[2]->attrs['id'] == $id) {
452 if($handler[1] === null) $handler[1] = $this;
453 $handler[1]->$handler[0]($this->xmlobj[2]);
454 #id handlers are only used once
455 unset($this->idhandlers[$id]);
456 break;
457 }
458 }
459 if(is_array($this->xmlobj)) {
460 $this->xmlobj = array_slice($this->xmlobj, 0, 1);
461 if(isset($this->xmlobj[0]) && $this->xmlobj[0] instanceof XMPPHP_XMLObj) {
462 $this->xmlobj[0]->subs = null;
463 }
464 }
465 unset($this->xmlobj[2]);
466 }
467 if($this->xml_depth == 0 and !$this->been_reset) {
468 if(!$this->disconnected) {
469 if(!$this->sent_disconnect) {
470 $this->send($this->stream_end);
471 }
472 $this->disconnected = true;
473 $this->sent_disconnect = true;
474 fclose($this->socket);
475 if($this->reconnect) {
476 $this->doReconnect();
477 }
478 }
479 $this->event('end_stream');
480 }
481 }
482
483 /**
484 * XML character callback
485 * @see xml_set_character_data_handler
486 *
487 * @param resource $parser
488 * @param string $data
489 */
490 public function charXML($parser, $data) {
491 if(array_key_exists($this->xml_depth, $this->xmlobj)) {
492 $this->xmlobj[$this->xml_depth]->data .= $data;
493 }
494 }
495
496 /**
497 * Event?
498 *
499 * @param string $name
500 * @param string $payload
501 */
502 public function event($name, $payload = null) {
503 $this->log->log("EVENT: $name", XMPPHP_Log::LEVEL_DEBUG);
504 foreach($this->eventhandlers as $handler) {
505 if($name == $handler[0]) {
506 if($handler[2] === null) {
507 $handler[2] = $this;
508 }
509 $handler[2]->$handler[1]($payload);
510 }
511 }
512 foreach($this->until as $key => $until) {
513 if(is_array($until)) {
514 if(in_array($name, $until)) {
515 $this->until_payload[$key][] = array($name, $payload);
516 $this->until[$key] = false;
517 }
518 }
519 }
520 }
521
522 /**
523 * Read from socket
524 */
525 public function read() {
526 $buff = @fread($this->socket, 1024);
527 if(!$buff) {
528 if($this->reconnect) {
529 $this->doReconnect();
530 } else {
531 fclose($this->socket);
532 return false;
533 }
534 }
535 $this->log->log("RECV: $buff", XMPPHP_Log::LEVEL_VERBOSE);
536 xml_parse($this->parser, $buff, false);
537 }
538
539 /**
540 * Send to socket
541 *
542 * @param string $msg
543 */
544 public function send($msg, $rec=false) {
545 if($this->time() - $this->last_send < .1) {
546 usleep(100000);
547 }
548 $wait = true;
549 while($wait) {
550 $read = null;
551 $write = array($this->socket);
552 $except = null;
553 $select = @stream_select($read, $write, $except, 0, 0);
554 if($select === False) {
555 $this->doReconnect();
556 return false;
557 } elseif ($select > 0) {
558 $wait = false;
559 } else {
560 usleep(100000);
561 //$this->processTime(.25);
562 }
563 }
564 $sentbytes = @fwrite($this->socket, $msg, 1024);
565 $this->last_send = $this->time();
566 $this->log->log("SENT: " . mb_substr($msg, 0, $sentbytes, '8bit'), XMPPHP_Log::LEVEL_VERBOSE);
567 if($sentbytes === FALSE) {
568 $this->doReconnect();
569 } elseif ($sentbytes != mb_strlen($msg, '8bit')) {
570 $this->send(mb_substr($msg, $sentbytes, mb_strlen($msg, '8bit'), '8bit'), true);
571 }
572 }
573
574 public function time() {
575 list($usec, $sec) = explode(" ", microtime());
576 return (float)$sec + (float)$usec;
577 }
578
579 /**
580 * Reset connection
581 */
582 public function reset() {
583 $this->xml_depth = 0;
584 unset($this->xmlobj);
585 $this->xmlobj = array();
586 $this->setupParser();
587 if(!$this->is_server) {
588 $this->send($this->stream_start);
589 }
590 $this->been_reset = true;
591 }
592
593 /**
594 * Setup the XML parser
595 */
596 public function setupParser() {
597 $this->parser = xml_parser_create('UTF-8');
598 xml_parser_set_option($this->parser, XML_OPTION_SKIP_WHITE, 1);
599 xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
600 xml_set_object($this->parser, $this);
601 xml_set_element_handler($this->parser, 'startXML', 'endXML');
602 xml_set_character_data_handler($this->parser, 'charXML');
603 }
604}