Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 399
0.00% covered (danger)
0.00%
0 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
jabber
0.00% covered (danger)
0.00%
0 / 399
0.00% covered (danger)
0.00%
0 / 37
32580
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 get_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_queue_object_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 stream_options
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 password
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 ssl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 port
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 username
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 server
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 can_use_ssl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 can_use_tls
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
42
 set_resource
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 connect
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 disconnect
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 connected
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 login
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_addresses
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
20
 to
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 reset
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 set_use_queue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 process_queue
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
132
 send
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
72
 send_xml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 open_socket
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 get_log
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 add_to_log
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 listen
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 register
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 send_presence
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 response
0.00% covered (danger)
0.00%
0 / 147
0.00% covered (danger)
0.00%
0 / 1
2862
 send_message
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 encrypt_password
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 parse_data
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 implode_data
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 xmlize
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 _xml_depth
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\messenger\method;
15
16/**
17 *
18 * Based on Jabber class from Flyspray project
19 *
20 * @version class.jabber2.php 1595 2008-09-19 (0.9.9)
21 * @copyright 2006 Flyspray.org
22 * @author Florian Schmitz (floele)
23 *
24 * Slightly modified by Acyd Burn (2006)
25 * Refactored to a service (2024)
26 */
27class jabber extends base
28{
29    /** @var string */
30    protected $connect_server;
31
32    /** @var resource|null */
33    protected $connection = null;
34
35    /** @var bool */
36    protected $enable_logging = true;
37
38    /** @var array */
39    protected $features = [];
40
41    /** @var array */
42    protected $jid = [];
43
44    /** @var array */
45    protected $log_array = [];
46
47    /** @var string */
48    protected $password;
49
50    /** @var int */
51    protected $port;
52
53    /** @var string */
54    protected $resource = 'functions_jabber.phpbb.php';
55
56    /** @var string */
57    protected $server;
58
59    /** @var array */
60    protected $session = [];
61
62    /** @var array */
63    protected $stream_options = [];
64
65    /** @var int */
66    protected $timeout = 10;
67
68    /** @var array */
69    protected $to = [];
70
71    /** @var bool */
72    protected $use_ssl = false;
73
74    /** @var string */
75    protected $username;
76
77    /** @var string Stream close handshake */
78    private const STREAM_CLOSE_HANDSHAKE = '</stream:stream>';
79
80    /**
81     * Set initial parameter values
82     * To init correctly, username() call should go before server()
83     * and ssl() call should go before port() and stream_options() calls.
84     *
85     * Example:
86     * $this->username($username)
87     *        ->password($password)
88     *        ->ssl($use_ssl)
89     *        ->server($server)
90     *        ->port($port)
91     *        ->stream_options(
92     *            'verify_peer' => true,
93     *            'verify_peer_name' => true,
94     *            'allow_self_signed' => false,
95     *        );
96     *
97     * @return void
98     */
99    public function init(): void
100    {
101        $this->username($this->config['jab_username'])
102            ->password($this->config['jab_password'])
103            ->ssl((bool) $this->config['jab_use_ssl'])
104            ->server($this->config['jab_host'])
105            ->port($this->config['jab_port'])
106            ->stream_options['ssl'] = [
107                'verify_peer' => $this->config['jab_verify_peer'],
108                'verify_peer_name' => $this->config['jab_verify_peer_name'],
109                'allow_self_signed' => $this->config['jab_allow_self_signed'],
110            ];
111    }
112
113    /**
114     * {@inheritDoc}
115     */
116    public function get_id(): int
117    {
118        return self::NOTIFY_IM;
119    }
120
121    /**
122     * {@inheritDoc}
123     */
124    public function get_queue_object_name(): string
125    {
126        return 'jabber';
127    }
128
129    /**
130     * {@inheritDoc}
131     */
132    public function is_enabled(): bool
133    {
134        return
135            !empty($this->config['jab_enable']) &&
136            !empty($this->config['jab_host']) &&
137            !empty($this->config['jab_username']) &&
138            !empty($this->config['jab_password']);
139    }
140
141    /**
142     * Set ssl context options
143     * See http://php.net/manual/en/context.ssl.php
144     *
145     * @param array $options SSL context options array
146     * @return self
147     */
148    public function stream_options(array $options = []): self
149    {
150        if ($this->use_ssl)
151        {
152            // Change default stream options if needed
153            $this->stream_options['ssl'] = array_merge($this->stream_options['ssl'], $options);
154        }
155
156        return $this;
157    }
158
159    /**
160     * Set password to connect to server
161     *
162     * @param string $password Password to connect to server
163     * @return self
164     */
165    public function password(string $password = ''): self
166    {
167        $this->password    = html_entity_decode($password, ENT_COMPAT);
168
169        return $this;
170    }
171
172    /**
173     * Set use of ssl to connect to server
174     *
175     * @param bool $use_ssl Flag indicating use of ssl to connect to server
176     * @return self
177     */
178    public function ssl(bool $use_ssl = false): self
179    {
180        $this->use_ssl = $use_ssl && self::can_use_ssl();
181
182        return $this;
183    }
184
185    /**
186     * Set port to connect to server
187     * use_ssl flag should be set first
188     *
189     * @param int $port Port to connect to server
190     * @return self
191     */
192    public function port(int $port = 5222): self
193    {
194        $this->port    = ($port > 0) ? $port : 5222;
195
196        // Change port if we use SSL
197        if ($this->port == 5222 && $this->use_ssl)
198        {
199            $this->port = 5223;
200        }
201
202        return $this;
203    }
204
205    /**
206     * Set username to connect to server
207     *
208     * @param string $username Username to connect to server
209     * @return self
210     */
211    public function username(string $username = ''): self
212    {
213        if (str_contains($username, '@'))
214        {
215            $this->jid = explode('@', $username, 2);
216            $this->username = $this->jid[0];
217        }
218        else
219        {
220            $this->username = $username;
221        }
222
223        return $this;
224    }
225
226    /**
227     * Set server to connect
228     * Username should be set first
229     *
230     * @param string $server Server to connect
231     * @return self
232     */
233    public function server(string $server = ''): self
234    {
235        $this->connect_server = $server ?: 'localhost';
236        $this->server = $this->jid[1] ?? $this->connect_server;
237
238        return $this;
239    }
240
241    /**
242     * Check if it's possible to use the SSL functionality
243     *
244     * @return bool
245     */
246    public static function can_use_ssl(): bool
247    {
248        return @extension_loaded('openssl');
249    }
250
251    /**
252     * Check if it's possible to use TLS functionality
253     *
254     * @return bool
255     */
256    public static function can_use_tls(): bool
257    {
258        if (!@extension_loaded('openssl') || !function_exists('stream_socket_enable_crypto') || !function_exists('stream_get_meta_data') || !function_exists('stream_set_blocking') || !function_exists('stream_get_wrappers'))
259        {
260            return false;
261        }
262
263        /**
264        * Make sure the encryption stream is supported
265        * Also seem to work without the crypto stream if correctly compiled
266
267        $streams = stream_get_wrappers();
268
269        if (!in_array('streams.crypto', $streams))
270        {
271            return false;
272        }
273        */
274
275        return true;
276    }
277
278    /**
279     * Sets the resource which is used. No validation is done here, only escaping
280     *
281     * @param string $name
282     * @return void
283     */
284    public function set_resource(string $name): void
285    {
286        $this->resource = $name;
287    }
288
289    /**
290     * Connect to the server
291     *
292     * @return bool
293     */
294    public function connect(): bool
295    {
296/*        if (!$this->check_jid($this->username . '@' . $this->server))
297        {
298            $this->add_to_log('Error: Jabber ID is not valid: ' . $this->username . '@' . $this->server);
299            return false;
300        }*/
301
302        $this->session['ssl'] = $this->use_ssl;
303
304        if ($this->open_socket($this->connect_server, $this->port))
305        {
306            $this->send_xml("<?xml version='1.0' encoding='UTF-8' ?" . ">\n");
307            $this->send_xml("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n");
308        }
309        else
310        {
311            $this->add_to_log('Error: connect() #2');
312            return false;
313        }
314
315        // Now we listen what the server has to say...and give appropriate responses
316        $this->response($this->listen());
317        return true;
318    }
319
320    /**
321     * Disconnect from the server
322     *
323     * @return bool
324     */
325    public function disconnect(): bool
326    {
327        if ($this->connected())
328        {
329            // disconnect gracefully
330            if (isset($this->session['sent_presence']))
331            {
332                $this->send_presence('offline', '', true);
333            }
334
335            $this->send(self::STREAM_CLOSE_HANDSHAKE);
336            // Check stream close handshake reply
337            $stream_close_reply = $this->listen();
338
339            if ($stream_close_reply != self::STREAM_CLOSE_HANDSHAKE)
340            {
341                $this->add_to_log("Error: Unexpected stream close handshake reply ”{$stream_close_reply}");
342            }
343
344            $this->session = [];
345            /** @psalm-suppress InvalidPropertyAssignmentValue */
346            return fclose($this->connection);
347        }
348
349        return false;
350    }
351
352    /**
353     * Check if it's still connected to the server
354     *
355     * @return bool
356     */
357    public function connected(): bool
358    {
359        return is_resource($this->connection) && !feof($this->connection);
360    }
361
362    /**
363     * Initiates login (using data from contructor, after calling connect())
364     *
365     * @return bool
366     */
367    public function login(): bool
368    {
369        if (empty($this->features))
370        {
371            $this->add_to_log('Error: No feature information from server available.');
372            return false;
373        }
374
375        return $this->response($this->features);
376    }
377
378    /**
379     * {@inheritDoc}
380     */
381    public function set_addresses(array $user_row): void
382    {
383        if (isset($user_row['user_jabber']) && $user_row['user_jabber'])
384        {
385            $this->to($user_row['user_jabber'], (isset($user_row['username']) ? $user_row['username'] : ''));
386        }
387    }
388
389    /**
390     * Sets jabber contact to send message to
391     *
392     * @param string    $address    Jabber "To" recipient address
393     * @param string    $realname    Jabber "To" recipient name
394     * @return void
395     */
396    public function to(string $address, string $realname = ''): void
397    {
398        // IM-Addresses could be empty
399        if (!trim($address))
400        {
401            return;
402        }
403
404        $pos = !empty($this->to) ? count($this->to) : 0;
405        $this->to[$pos]['uid'] = trim($address);
406        $this->to[$pos]['name'] = trim($realname);
407    }
408
409    /**
410     * Resets all the data (address, template file, etc) to default
411     */
412    public function reset(): void
413    {
414        $this->subject = $this->msg = '';
415        $this->additional_headers = $this->to = [];
416        $this->use_queue = true;
417        unset($this->template);
418    }
419
420    /**
421     * {@inheritDoc}
422     */
423    public function set_use_queue(bool $use_queue = true): void
424    {
425        $this->use_queue = !$this->config['jab_package_size'] ? false : $use_queue;
426    }
427
428    /**
429     * {@inheritDoc}
430     */
431    public function process_queue(array &$queue_data): void
432    {
433        $queue_object_name = $this->get_queue_object_name();
434        $messages_count = count($queue_data[$queue_object_name]['data']);
435
436        if (!$this->is_enabled() || !$messages_count)
437        {
438            unset($queue_data[$queue_object_name]);
439            return;
440        }
441
442        @set_time_limit(0);
443
444        $package_size = $queue_data[$queue_object_name]['package_size'] ?? 0;
445        $num_items = (!$package_size || $messages_count < $package_size) ? $messages_count : $package_size;
446
447        for ($i = 0; $i < $num_items; $i++)
448        {
449            // Make variables available...
450            extract(array_shift($queue_data[$queue_object_name]['data']));
451
452            if (!$this->connect())
453            {
454                $this->error($this->user->lang['ERR_JAB_CONNECT'] . '<br />' . $this->get_log());
455                return;
456            }
457
458            if (!$this->login())
459            {
460                $this->error($this->user->lang['ERR_JAB_AUTH'] . '<br />' . $this->get_log());
461                return;
462            }
463
464            foreach ($addresses as $address)
465            {
466                if ($this->send_message($address, $msg, $subject) === false)
467                {
468                    $this->error($this->get_log());
469                    continue;
470                }
471            }
472        }
473
474        // No more data for this object? Unset it
475        if (!count($queue_data[$queue_object_name]['data']))
476        {
477            unset($queue_data[$queue_object_name]);
478        }
479
480        $this->disconnect();
481    }
482
483    /**
484     * {@inheritDoc}
485     */
486    public function send(): bool
487    {
488        $this->prepare_message();
489
490        if (empty($this->to))
491        {
492            $this->add_to_log('Error: Could not send, recepient addresses undefined.');
493            return false;
494        }
495
496        $addresses = [];
497        foreach ($this->to as $uid_ary)
498        {
499            $addresses[] = $uid_ary['uid'];
500        }
501        $addresses = array_unique($addresses);
502
503        if (!$this->use_queue)
504        {
505            if (!$this->connect())
506            {
507                $this->error($this->user->lang['ERR_JAB_CONNECT'] . '<br />' . $this->get_log());
508                return false;
509            }
510
511            if (!$this->login())
512            {
513                $this->error($this->user->lang['ERR_JAB_AUTH'] . '<br />' . $this->get_log());
514                return false;
515            }
516
517            foreach ($addresses as $address)
518            {
519                if ($this->send_message($address, $this->msg, $this->subject) === false)
520                {
521                    $this->error($this->get_log());
522                    continue;
523                }
524            }
525
526            $this->disconnect();
527        }
528        else
529        {
530            $this->queue->init('jabber', $this->config['jab_package_size']);
531            $this->queue->put('jabber', [
532                'addresses'        => $addresses,
533                'subject'        => $this->subject,
534                'msg'            => $this->msg,
535            ]);
536        }
537        unset($addresses);
538
539        $this->reset();
540
541        return true;
542    }
543
544    /**
545     * Send data to the Jabber server
546     *
547     * @param string $xml
548     * @return int|bool
549     */
550    public function send_xml(string $xml): int|bool
551    {
552        if ($this->connected())
553        {
554            $xml = trim($xml);
555            return fwrite($this->connection, $xml);
556        }
557        else
558        {
559            $this->add_to_log('Error: Could not send, connection lost (flood?).');
560            return false;
561        }
562    }
563
564    /**
565     * Open socket
566     *
567     * @param string $server Host to connect to
568     * @param int $port Port number
569     *
570     * @return bool
571     */
572    public function open_socket(string $server, int $port): bool
573    {
574        if (@function_exists('dns_get_record'))
575        {
576            $record = @dns_get_record("_xmpp-client._tcp.$server", DNS_SRV);
577            if (!empty($record) && !empty($record[0]['target']))
578            {
579                $server = $record[0]['target'];
580            }
581        }
582
583        $remote_socket = $this->use_ssl ? 'ssl://' . $server . ':' . $port : $server . ':' . $port;
584        $socket_context = stream_context_create($this->stream_options);
585
586        if ($this->connection = @stream_socket_client($remote_socket, $errorno, $errorstr, $this->timeout, STREAM_CLIENT_CONNECT, $socket_context))
587        {
588            stream_set_blocking($this->connection, 0);
589            stream_set_timeout($this->connection, 60);
590
591            return true;
592        }
593
594        // Apparently an error occurred...
595        $this->add_to_log('Error: open_socket() - ' . $errorstr);
596        return false;
597    }
598
599    /**
600     * Get connection log
601     *
602     * @return string
603     */
604    public function get_log(): string
605    {
606        if ($this->enable_logging && count($this->log_array))
607        {
608            return implode("<br /><br />", $this->log_array);
609        }
610
611        return '';
612    }
613
614    /**
615     * Add information to log
616     *
617     * @param string $string Log entry
618     * @return void
619     */
620    protected function add_to_log(string $string): void
621    {
622        if ($this->enable_logging)
623        {
624            $this->log_array[] = utf8_htmlspecialchars($string);
625        }
626    }
627
628    /**
629     * Listens to the connection until it gets data or the timeout is reached.
630     * Thus, it should only be called if data is expected to be received.
631     *
632     * @param int $timeout Connection timeout
633     * @param bool $wait Flag indicating if it should wait for the responce until timeout
634     * @return bool|array Either false for timeout or an array with the received data
635     */
636    public function listen(int $timeout = 10, bool $wait = false): bool|array
637    {
638        if (!$this->connected())
639        {
640            return false;
641        }
642
643        // Wait for a response until timeout is reached
644        $start = time();
645        $data = '';
646
647        do
648        {
649            $read = trim(fread($this->connection, 4096));
650            $data .= $read;
651        }
652        while (time() <= $start + $timeout && !feof($this->connection) && ($wait || $data == '' || $read != '' || (substr(rtrim($data), -1) != '>')));
653
654        if ($data != '')
655        {
656            return $this->xmlize($data);
657        }
658        else
659        {
660            $this->add_to_log('Timeout, no response from server.');
661            return false;
662        }
663    }
664
665    /**
666     * Initiates account registration (based on data used for constructor)
667     *
668     * @return bool|null
669     */
670    public function register(): bool|null
671    {
672        if (!isset($this->session['id']) || isset($this->session['jid']))
673        {
674            $this->add_to_log('Error: Cannot initiate registration.');
675            return false;
676        }
677
678        $this->send_xml("<iq type='get' id='reg_1'><query xmlns='jabber:iq:register'/></iq>");
679        return $this->response($this->listen());
680    }
681
682    /**
683     * Sets account presence. No additional info required (default is "online" status)
684     *
685     * @param string    $message        Account status (online, offline)
686     * @param string    $type            Status type (dnd, away, chat, xa or nothing)
687     * @param bool        $unavailable    Set to true to make unavailable status
688     * @return int|bool
689     */
690    function send_presence(string $message = '', string $type = '', bool $unavailable = false): int|bool
691    {
692        if (!isset($this->session['jid']))
693        {
694            $this->add_to_log('ERROR: send_presence() - Cannot set presence at this point, no jid given.');
695            return false;
696        }
697
698        $type = strtolower($type);
699        $type = (in_array($type, array('dnd', 'away', 'chat', 'xa'))) ? '<show>'. $type .'</show>' : '';
700
701        $unavailable = ($unavailable) ? " type='unavailable'" : '';
702        $message = ($message) ? '<status>' . utf8_htmlspecialchars($message) .'</status>' : '';
703
704        $this->session['sent_presence'] = !$unavailable;
705
706        return $this->send_xml("<presence$unavailable>" . $type . $message . '</presence>');
707    }
708
709    /**
710     * This handles all the different XML elements
711     *
712     * @param array $xml
713     * @return bool
714     */
715    function response(array $xml): bool
716    {
717        if (!count($xml))
718        {
719            return false;
720        }
721
722        // did we get multiple elements? do one after another
723        // array('message' => ..., 'presence' => ...)
724        if (count($xml) > 1)
725        {
726            foreach ($xml as $key => $value)
727            {
728                $this->response(array($key => $value));
729            }
730            return true;
731        }
732        else if (is_array(reset($xml)) && count(reset($xml)) > 1)
733        {
734            // or even multiple elements of the same type?
735            // array('message' => array(0 => ..., 1 => ...))
736            foreach (reset($xml) as $value)
737            {
738                $this->response(array(key($xml) => array(0 => $value)));
739            }
740            return true;
741        }
742
743        switch (key($xml))
744        {
745            case 'stream:stream':
746                // Connection initialised (or after authentication). Not much to do here...
747
748                if (isset($xml['stream:stream'][0]['#']['stream:features']))
749                {
750                    // we already got all info we need
751                    $this->features = $xml['stream:stream'][0]['#'];
752                }
753                else
754                {
755                    $this->features = $this->listen();
756                }
757
758                $second_time = isset($this->session['id']);
759                $this->session['id'] = isset($xml['stream:stream'][0]['@']['id']) ? $xml['stream:stream'][0]['@']['id'] : '';
760
761                if ($second_time)
762                {
763                    // If we are here for the second time after TLS, we need to continue logging in
764                    return $this->login();
765                }
766
767                // go on with authentication?
768                if (isset($this->features['stream:features'][0]['#']['bind']) || !empty($this->session['tls']))
769                {
770                    return $this->response($this->features);
771                }
772                return false;
773            break;
774
775            case 'stream:features':
776                // Resource binding after successful authentication
777                if (isset($this->session['authenticated']))
778                {
779                    // session required?
780                    $this->session['sess_required'] = isset($xml['stream:features'][0]['#']['session']);
781
782                    $this->send_xml("<iq type='set' id='bind_1'>
783                        <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
784                            <resource>" . utf8_htmlspecialchars($this->resource) . '</resource>
785                        </bind>
786                    </iq>');
787                    return $this->response($this->listen());
788                }
789
790                // Let's use TLS if SSL is not enabled and we can actually use it
791                if (!$this->session['ssl'] && self::can_use_tls() && self::can_use_ssl() && isset($xml['stream:features'][0]['#']['starttls']))
792                {
793                    $this->add_to_log('Switching to TLS.');
794                    $this->send_xml("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>\n");
795                    return $this->response($this->listen());
796                }
797
798                // Does the server support SASL authentication?
799
800                // I hope so, because we do (and no other method).
801                if (isset($xml['stream:features'][0]['#']['mechanisms'][0]['@']['xmlns']) && $xml['stream:features'][0]['#']['mechanisms'][0]['@']['xmlns'] == 'urn:ietf:params:xml:ns:xmpp-sasl')
802                {
803                    // Now decide on method
804                    $methods = array();
805
806                    foreach ($xml['stream:features'][0]['#']['mechanisms'][0]['#']['mechanism'] as $value)
807                    {
808                        $methods[] = $value['#'];
809                    }
810
811                    // we prefer DIGEST-MD5
812                    // we don't want to use plain authentication (neither does the server usually) if no encryption is in place
813
814                    // http://www.xmpp.org/extensions/attic/jep-0078-1.7.html
815                    // The plaintext mechanism SHOULD NOT be used unless the underlying stream is encrypted (using SSL or TLS)
816                    // and the client has verified that the server certificate is signed by a trusted certificate authority.
817
818                    if (in_array('DIGEST-MD5', $methods))
819                    {
820                        $this->send_xml("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>");
821                    }
822                    else if (in_array('PLAIN', $methods) && ($this->session['ssl'] || !empty($this->session['tls'])))
823                    {
824                        // http://www.ietf.org/rfc/rfc4616.txt (PLAIN SASL Mechanism)
825                        $this->send_xml("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"
826                            . base64_encode($this->username . '@' . $this->server . chr(0) . $this->username . chr(0) . $this->password) .
827                            '</auth>');
828                    }
829                    else if (in_array('ANONYMOUS', $methods))
830                    {
831                        $this->send_xml("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>");
832                    }
833                    else
834                    {
835                        // not good...
836                        $this->add_to_log('Error: No authentication method supported.');
837                        $this->disconnect();
838                        return false;
839                    }
840
841                    return $this->response($this->listen());
842                }
843                else
844                {
845                    // ok, this is it. bye.
846                    $this->add_to_log('Error: Server does not offer SASL authentication.');
847                    $this->disconnect();
848                    return false;
849                }
850            break;
851
852            case 'challenge':
853                // continue with authentication...a challenge literally -_-
854                $decoded = base64_decode($xml['challenge'][0]['#']);
855                $decoded = $this->parse_data($decoded);
856
857                if (!isset($decoded['digest-uri']))
858                {
859                    $decoded['digest-uri'] = 'xmpp/'. $this->server;
860                }
861
862                // better generate a cnonce, maybe it's needed
863                $decoded['cnonce'] = base64_encode(md5(uniqid(mt_rand(), true)));
864
865                // second challenge?
866                if (isset($decoded['rspauth']))
867                {
868                    $this->send_xml("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
869                }
870                else
871                {
872                    // Make sure we only use 'auth' for qop (relevant for $this->encrypt_password())
873                    // If the <response> is choking up on the changed parameter we may need to adjust encrypt_password() directly
874                    if (isset($decoded['qop']) && $decoded['qop'] != 'auth' && strpos($decoded['qop'], 'auth') !== false)
875                    {
876                        $decoded['qop'] = 'auth';
877                    }
878
879                    $response = array(
880                        'username'    => $this->username,
881                        'response'    => $this->encrypt_password(array_merge($decoded, array('nc' => '00000001'))),
882                        'charset'    => 'utf-8',
883                        'nc'        => '00000001',
884                        'qop'        => 'auth',            // only auth being supported
885                    );
886
887                    foreach (array('nonce', 'digest-uri', 'realm', 'cnonce') as $key)
888                    {
889                        if (isset($decoded[$key]))
890                        {
891                            $response[$key] = $decoded[$key];
892                        }
893                    }
894
895                    $this->send_xml("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" . base64_encode($this->implode_data($response)) . '</response>');
896                }
897
898                return $this->response($this->listen());
899            break;
900
901            case 'failure':
902                $this->add_to_log('Error: Server sent "failure".');
903                $this->disconnect();
904                return false;
905            break;
906
907            case 'proceed':
908                // continue switching to TLS
909                $meta = stream_get_meta_data($this->connection);
910                stream_set_blocking($this->connection, 1);
911
912                if (!stream_socket_enable_crypto($this->connection, true, STREAM_CRYPTO_METHOD_TLS_CLIENT))
913                {
914                    $this->add_to_log('Error: TLS mode change failed.');
915                    return false;
916                }
917
918                stream_set_blocking($this->connection, $meta['blocked']);
919                $this->session['tls'] = true;
920
921                // new stream
922                $this->send_xml("<?xml version='1.0' encoding='UTF-8' ?" . ">\n");
923                $this->send_xml("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n");
924
925                return $this->response($this->listen());
926            break;
927
928            case 'success':
929                // Yay, authentication successful.
930                $this->send_xml("<stream:stream to='{$this->server}' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>\n");
931                $this->session['authenticated'] = true;
932
933                // we have to wait for another response
934                return $this->response($this->listen());
935            break;
936
937            case 'iq':
938                // we are not interested in IQs we did not expect
939                if (!isset($xml['iq'][0]['@']['id']))
940                {
941                    return false;
942                }
943
944                // multiple possibilities here
945                switch ($xml['iq'][0]['@']['id'])
946                {
947                    case 'bind_1':
948                        $this->session['jid'] = $xml['iq'][0]['#']['bind'][0]['#']['jid'][0]['#'];
949
950                        // and (maybe) yet another request to be able to send messages *finally*
951                        if ($this->session['sess_required'])
952                        {
953                            $this->send_xml("<iq to='{$this->server}' type='set' id='sess_1'>
954                                <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
955                                </iq>");
956                            return $this->response($this->listen());
957                        }
958
959                        return true;
960                    break;
961
962                    case 'sess_1':
963                        return true;
964                    break;
965
966                    case 'reg_1':
967                        $this->send_xml("<iq type='set' id='reg_2'>
968                                <query xmlns='jabber:iq:register'>
969                                    <username>" . utf8_htmlspecialchars($this->username) . "</username>
970                                    <password>" . utf8_htmlspecialchars($this->password) . "</password>
971                                </query>
972                            </iq>");
973                        return $this->response($this->listen());
974                    break;
975
976                    case 'reg_2':
977                        // registration end
978                        if (isset($xml['iq'][0]['#']['error']))
979                        {
980                            $this->add_to_log('Warning: Registration failed.');
981                            return false;
982                        }
983                        return true;
984                    break;
985
986                    case 'unreg_1':
987                        return true;
988                    break;
989
990                    default:
991                        $this->add_to_log('Notice: Received unexpected IQ.');
992                        return false;
993                    break;
994                }
995            break;
996
997            case 'message':
998                // we are only interested in content...
999                if (!isset($xml['message'][0]['#']['body']))
1000                {
1001                    return false;
1002                }
1003
1004                $message['body'] = $xml['message'][0]['#']['body'][0]['#'];
1005                $message['from'] = $xml['message'][0]['@']['from'];
1006
1007                if (isset($xml['message'][0]['#']['subject']))
1008                {
1009                    $message['subject'] = $xml['message'][0]['#']['subject'][0]['#'];
1010                }
1011                $this->session['messages'][] = $message;
1012                return true;
1013            break;
1014
1015            default:
1016                // hm...don't know this response
1017                $this->add_to_log('Notice: Unknown server response');
1018                return false;
1019            break;
1020        }
1021    }
1022
1023    /**
1024     * Send Jabber message
1025     *
1026     * @param string $to        Recepient usermane
1027     * @param string $text        Message text
1028     * @param string $subject    Message subject
1029     * @param string $type        Message type
1030     *
1031     * @return int|bool
1032     */
1033    public function send_message(string $to, string $text, string $subject = '', string $type = 'normal'): int|bool
1034    {
1035        if (!isset($this->session['jid']))
1036        {
1037            return false;
1038        }
1039
1040        if (!in_array($type, array('chat', 'normal', 'error', 'groupchat', 'headline')))
1041        {
1042            $type = 'normal';
1043        }
1044
1045        return $this->send_xml("<message from='" . utf8_htmlspecialchars($this->session['jid']) . "' to='" . utf8_htmlspecialchars($to) . "' type='$type' id='" . uniqid('msg') . "'>
1046            <subject>" . utf8_htmlspecialchars($subject) . "</subject>
1047            <body>" . utf8_htmlspecialchars($text) . "</body>
1048            </message>"
1049        );
1050    }
1051
1052    /**
1053     * Encrypts a password as in RFC 2831
1054     *
1055     * @param array $data Needs data from the client-server connection
1056     * @return string
1057     */
1058    public function encrypt_password(array $data): string
1059    {
1060        // let's me think about <challenge> again...
1061        foreach (array('realm', 'cnonce', 'digest-uri') as $key)
1062        {
1063            if (!isset($data[$key]))
1064            {
1065                $data[$key] = '';
1066            }
1067        }
1068
1069        $pack = md5($this->username . ':' . $data['realm'] . ':' . $this->password);
1070
1071        if (isset($data['authzid']))
1072        {
1073            $a1 = pack('H32', $pack)  . sprintf(':%s:%s:%s', $data['nonce'], $data['cnonce'], $data['authzid']);
1074        }
1075        else
1076        {
1077            $a1 = pack('H32', $pack)  . sprintf(':%s:%s', $data['nonce'], $data['cnonce']);
1078        }
1079
1080        // should be: qop = auth
1081        $a2 = 'AUTHENTICATE:'. $data['digest-uri'];
1082
1083        return md5(sprintf('%s:%s:%s:%s:%s:%s', md5($a1), $data['nonce'], $data['nc'], $data['cnonce'], $data['qop'], md5($a2)));
1084    }
1085
1086    /**
1087     * Parse data string like a="b",c="d",... or like a="a, b", c, d="e", f=g,...
1088     *
1089     * @param string $data
1090     * @return array a => b ...
1091     */
1092    public function parse_data(string $data): array
1093    {
1094        $data = explode(',', $data);
1095        $pairs = array();
1096        $key = false;
1097
1098        foreach ($data as $pair)
1099        {
1100            $dd = strpos($pair, '=');
1101
1102            if ($dd)
1103            {
1104                $key = trim(substr($pair, 0, $dd));
1105                $pairs[$key] = trim(trim(substr($pair, $dd + 1)), '"');
1106            }
1107            else if (strpos(strrev(trim($pair)), '"') === 0 && $key)
1108            {
1109                // We are actually having something left from "a, b" values, add it to the last one we handled.
1110                $pairs[$key] .= ',' . trim(trim($pair), '"');
1111                continue;
1112            }
1113        }
1114
1115        return $pairs;
1116    }
1117
1118    /**
1119     * The opposite of jabber::parse_data()
1120     *
1121     * @param array $data Data array
1122     * @return string
1123     */
1124    public function implode_data(array $data): string
1125    {
1126        $return = array();
1127        foreach ($data as $key => $value)
1128        {
1129            $return[] = $key . '="' . $value . '"';
1130        }
1131        return implode(',', $return);
1132    }
1133
1134    /**
1135     * xmlize()
1136     * @author Hans Anderson
1137     * @copyright Hans Anderson / http://www.hansanderson.com/php/xml/
1138     *
1139     * @param string $data Data string
1140     * @param string|int|bool $skip_white New XML parser option value
1141     * @param string $encoding Encoding value
1142     * @return array
1143     */
1144    function xmlize(string $data, string|int|bool $skip_white = 1, string $encoding = 'UTF-8'): array
1145    {
1146        $data = trim($data);
1147
1148        if (substr($data, 0, 5) != '<?xml')
1149        {
1150            // mod
1151            $data = '<root>'. $data . '</root>';
1152        }
1153
1154        $vals = $index = $array = array();
1155        $parser = xml_parser_create($encoding);
1156        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
1157        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, $skip_white);
1158        xml_parse_into_struct($parser, $data, $vals, $index);
1159        xml_parser_free($parser);
1160
1161        $i = 0;
1162        $tagname = $vals[$i]['tag'];
1163
1164        $array[$tagname][0]['@'] = (isset($vals[$i]['attributes'])) ? $vals[$i]['attributes'] : array();
1165        $array[$tagname][0]['#'] = $this->_xml_depth($vals, $i);
1166
1167        if (substr($data, 0, 5) != '<?xml')
1168        {
1169            $array = $array['root'][0]['#'];
1170        }
1171
1172        return $array;
1173    }
1174
1175    /**
1176     * _xml_depth()
1177     * @author Hans Anderson
1178     * @copyright Hans Anderson / http://www.hansanderson.com/php/xml/
1179     *
1180     * @param array $vals XML data array
1181     * @param int $i XML tags depth level
1182     * @return array
1183     */
1184    function _xml_depth(array $vals, int &$i): array
1185    {
1186        $children = array();
1187
1188        if (isset($vals[$i]['value']))
1189        {
1190            array_push($children, $vals[$i]['value']);
1191        }
1192
1193        while (++$i < count($vals))
1194        {
1195            switch ($vals[$i]['type'])
1196            {
1197                case 'open':
1198
1199                    $tagname = (isset($vals[$i]['tag'])) ? $vals[$i]['tag'] : '';
1200                    $size = (isset($children[$tagname])) ? count($children[$tagname]) : 0;
1201
1202                    if (isset($vals[$i]['attributes']))
1203                    {
1204                        $children[$tagname][$size]['@'] = $vals[$i]['attributes'];
1205                    }
1206
1207                    $children[$tagname][$size]['#'] = $this->_xml_depth($vals, $i);
1208
1209                break;
1210
1211                case 'cdata':
1212                    array_push($children, $vals[$i]['value']);
1213                break;
1214
1215                case 'complete':
1216
1217                    $tagname = $vals[$i]['tag'];
1218                    $size = (isset($children[$tagname])) ? count($children[$tagname]) : 0;
1219                    $children[$tagname][$size]['#'] = (isset($vals[$i]['value'])) ? $vals[$i]['value'] : array();
1220
1221                    if (isset($vals[$i]['attributes']))
1222                    {
1223                        $children[$tagname][$size]['@'] = $vals[$i]['attributes'];
1224                    }
1225
1226                break;
1227
1228                case 'close':
1229                    return $children;
1230                break;
1231            }
1232        }
1233
1234        return $children;
1235    }
1236}