Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 399 |
|
0.00% |
0 / 37 |
CRAP | |
0.00% |
0 / 1 |
jabber | |
0.00% |
0 / 399 |
|
0.00% |
0 / 37 |
32580 | |
0.00% |
0 / 1 |
init | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
get_id | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
get_queue_object_name | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
is_enabled | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
stream_options | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
password | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
ssl | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
port | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
username | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
server | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
can_use_ssl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
can_use_tls | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
42 | |||
set_resource | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
connect | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
disconnect | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
connected | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
login | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
set_addresses | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
20 | |||
to | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
reset | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
set_use_queue | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
process_queue | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
132 | |||
send | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
72 | |||
send_xml | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
open_socket | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
get_log | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
add_to_log | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
listen | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
72 | |||
register | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
send_presence | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
response | |
0.00% |
0 / 147 |
|
0.00% |
0 / 1 |
2862 | |||
send_message | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
encrypt_password | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
parse_data | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
implode_data | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
xmlize | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
_xml_depth | |
0.00% |
0 / 26 |
|
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 | |
14 | namespace 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 | */ |
27 | class 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 | } |