XMPP.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  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. * @author Michael Garvin <JID: gar@netflint.net>
  26. * @author Alexander Birkner (https://github.com/BirknerAlex)
  27. * @author zorn-v (https://github.com/zorn-v/xmpphp/)
  28. * @author GNU social
  29. * @copyright 2008 Nathanael C. Fritz
  30. */
  31. namespace XMPPHP;
  32. /** XMPPHP_XMLStream */
  33. require_once __DIR__ . DIRECTORY_SEPARATOR . 'XMLStream.php';
  34. require_once __DIR__ . DIRECTORY_SEPARATOR . 'Roster.php';
  35. /**
  36. * XMPPHP XMPP
  37. *
  38. * @package XMPPHP
  39. * @author Nathanael C. Fritz <JID: fritzy@netflint.net>
  40. * @author Stephan Wentz <JID: stephan@jabber.wentz.it>
  41. * @author Michael Garvin <JID: gar@netflint.net>
  42. * @copyright 2008 Nathanael C. Fritz
  43. * @version $Id$
  44. */
  45. class XMPP extends XMLStream
  46. {
  47. /**
  48. * @var string
  49. */
  50. public $server;
  51. /**
  52. * @var string
  53. */
  54. public $user;
  55. /**
  56. * @var bool
  57. */
  58. public $track_presence = true;
  59. /**
  60. * @var object
  61. */
  62. public $roster;
  63. /**
  64. * @var string
  65. */
  66. protected $password;
  67. /**
  68. * @var string
  69. */
  70. protected $resource;
  71. /**
  72. * @var string
  73. */
  74. protected $fulljid;
  75. /**
  76. * @var string
  77. */
  78. protected $basejid;
  79. /**
  80. * @var bool
  81. */
  82. protected $authed = false;
  83. protected $session_started = false;
  84. /**
  85. * @var bool
  86. */
  87. protected $auto_subscribe = false;
  88. /**
  89. * @var bool
  90. */
  91. protected $use_encryption = true;
  92. /**
  93. * Constructor
  94. *
  95. * @param string $host
  96. * @param integer $port
  97. * @param string $user
  98. * @param string $password
  99. * @param string $resource
  100. * @param string $server
  101. * @param bool $print_log
  102. * @param string $log_level
  103. */
  104. public function __construct(
  105. string $host,
  106. int $port,
  107. string $user,
  108. string $password,
  109. string $resource,
  110. ?string $server = null,
  111. bool $print_log = false,
  112. ?string $log_level = null
  113. ) {
  114. parent::__construct($host, $port, $print_log, $log_level);
  115. $this->user = $user;
  116. $this->password = $password;
  117. $this->resource = $resource;
  118. if (!$server) {
  119. $server = $host;
  120. }
  121. $this->server = $server;
  122. $this->basejid = $this->user . '@' . $this->host;
  123. $this->roster = new Roster();
  124. $this->track_presence = true;
  125. $this->stream_start = '<stream:stream to="' . $server . '" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0">';
  126. $this->stream_end = '</stream:stream>';
  127. $this->default_ns = 'jabber:client';
  128. $this->addXPathHandler(
  129. '{http://etherx.jabber.org/streams}features',
  130. [$this, 'features_handler']
  131. );
  132. $this->addXPathHandler(
  133. '{urn:ietf:params:xml:ns:xmpp-sasl}success',
  134. [$this, 'sasl_success_handler']
  135. );
  136. $this->addXPathHandler(
  137. '{urn:ietf:params:xml:ns:xmpp-sasl}failure',
  138. [$this, 'sasl_failure_handler']
  139. );
  140. $this->addXPathHandler(
  141. '{urn:ietf:params:xml:ns:xmpp-tls}proceed',
  142. [$this, 'tls_proceed_handler']
  143. );
  144. $this->addXPathHandler(
  145. '{jabber:client}message',
  146. [$this, 'message_handler']
  147. );
  148. $this->addXPathHandler(
  149. '{jabber:client}presence',
  150. [$this, 'presence_handler']
  151. );
  152. $this->addXPathHandler(
  153. 'iq/{jabber:iq:roster}query',
  154. [$this, 'roster_iq_handler']
  155. );
  156. }
  157. /**
  158. * Turn encryption on/ff
  159. *
  160. * @param bool $useEncryption (optional)
  161. */
  162. public function useEncryption(bool $useEncryption = true): void
  163. {
  164. $this->use_encryption = $useEncryption;
  165. }
  166. /**
  167. * Turn on auto-authorization of subscription requests.
  168. *
  169. * @param bool $autoSubscribe (optional)
  170. */
  171. public function autoSubscribe(bool $autoSubscribe = true): void
  172. {
  173. $this->auto_subscribe = $autoSubscribe;
  174. }
  175. /**
  176. * Send XMPP Message
  177. *
  178. * @param string $to
  179. * @param string $body
  180. * @param string $type (optional)
  181. * @param string|null $subject (optional)
  182. * @param string|null $payload (optional)
  183. * @throws Exception
  184. */
  185. public function message(string $to, string $body, string $type = 'chat', ?string $subject = null, ?string $payload = null): void
  186. {
  187. if ($this->disconnected) {
  188. throw new Exception('You need to connect first');
  189. }
  190. if (empty($type)) {
  191. $type = 'chat';
  192. }
  193. $to = htmlspecialchars($to);
  194. $body = htmlspecialchars($body);
  195. $subject = htmlspecialchars($subject);
  196. $subject = ($subject) ? '<subject>' . $subject . '</subject>' : '';
  197. $payload = ($payload) ? $payload : '';
  198. $sprintf = '<message from="%s" to="%s" type="%s">%s<body>%s</body>%s</message>';
  199. $output = sprintf($sprintf, $this->fulljid, $to, $type, $subject, $body, $payload);
  200. $this->send($output);
  201. }
  202. /**
  203. * Set Presence
  204. *
  205. * @param string $status
  206. * @param string $show
  207. * @param string $to
  208. * @param string $type
  209. * @param null $priority
  210. * @throws Exception
  211. */
  212. public function presence($status = null, $show = 'available', $to = null, $type = 'available', $priority = null): void
  213. {
  214. if ($this->disconnected) {
  215. throw new Exception('You need to connect first');
  216. }
  217. if ($type == 'available') {
  218. $type = '';
  219. }
  220. $to = htmlspecialchars($to);
  221. $status = htmlspecialchars($status);
  222. if ($show == 'unavailable') {
  223. $type = 'unavailable';
  224. }
  225. $out = "<presence";
  226. if ($to) {
  227. $out .= " to=\"$to\"";
  228. }
  229. if ($type) {
  230. $out .= " type='$type'";
  231. }
  232. if ($show == 'available' and !$status and $priority !== null) {
  233. $out .= "/>";
  234. } else {
  235. $out .= ">";
  236. if ($show != 'available') {
  237. $out .= "<show>$show</show>";
  238. }
  239. if ($status) {
  240. $out .= "<status>$status</status>";
  241. }
  242. if ($priority !== null) {
  243. $out .= "<priority>$priority</priority>";
  244. }
  245. $out .= "</presence>";
  246. }
  247. $this->send($out);
  248. }
  249. /**
  250. * Send Auth request
  251. *
  252. * @param string $jid
  253. * @throws Exception
  254. */
  255. public function subscribe(string $jid): void
  256. {
  257. $this->send("<presence type='subscribe' to='{$jid}' from='{$this->fulljid}' />");
  258. #$this->send("<presence type='subscribed' to='{$jid}' from='{$this->fulljid}' />");
  259. }
  260. /**
  261. * Message handler
  262. *
  263. * @param XMLObj $xml
  264. */
  265. public function message_handler(XMLObj $xml): void
  266. {
  267. if (isset($xml->attrs['type'])) {
  268. $payload['type'] = $xml->attrs['type'];
  269. } else {
  270. $payload['type'] = 'chat';
  271. }
  272. $body = $xml->sub('body');
  273. $payload['from'] = $xml->attrs['from'];
  274. $payload['body'] = is_object($body) ? $body->data : false; // $xml->sub('body')->data;
  275. $payload['xml'] = $xml;
  276. $this->log->log("Message: {$payload['body']}", Log::LEVEL_DEBUG);
  277. $this->event('message', $payload);
  278. }
  279. /**
  280. * Presence handler
  281. *
  282. * @param XMLObj $xml
  283. * @throws Exception
  284. */
  285. public function presence_handler(XMLObj $xml): void
  286. {
  287. $payload['type'] = (isset($xml->attrs['type'])) ? $xml->attrs['type'] : 'available';
  288. $payload['show'] = (isset($xml->sub('show')->data)) ? $xml->sub('show')->data : $payload['type'];
  289. $payload['from'] = $xml->attrs['from'];
  290. $payload['status'] = (isset($xml->sub('status')->data)) ? $xml->sub('status')->data : '';
  291. $payload['priority'] = (isset($xml->sub('priority')->data)) ? intval($xml->sub('priority')->data) : 0;
  292. $payload['xml'] = $xml;
  293. if ($this->track_presence) {
  294. $this->roster->setPresence($payload['from'], $payload['priority'], $payload['show'], $payload['status']);
  295. }
  296. $this->log->log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}", Log::LEVEL_DEBUG);
  297. if (array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribe') {
  298. if ($this->auto_subscribe) {
  299. $this->send("<presence type='subscribed' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
  300. $this->send("<presence type='subscribe' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
  301. }
  302. $this->event('subscription_requested', $payload);
  303. } elseif (array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribed') {
  304. $this->event('subscription_accepted', $payload);
  305. } else {
  306. $this->event('presence', $payload);
  307. }
  308. }
  309. /**
  310. * Retrieves the roster
  311. *
  312. * @throws Exception
  313. */
  314. public function getRoster(): void
  315. {
  316. $id = $this->getID();
  317. $this->send("<iq xmlns='jabber:client' type='get' id='$id'><query xmlns='jabber:iq:roster' /></iq>");
  318. }
  319. /**
  320. * Retrieves the vcard
  321. * @param string|null $jid
  322. * @throws Exception
  323. */
  324. public function getVCard(?string $jid = null): void
  325. {
  326. $id = $this->getId();
  327. $this->addIdHandler($id, [$this, 'vcard_get_handler']);
  328. if ($jid) {
  329. $this->send("<iq type='get' id='$id' to='$jid'><vCard xmlns='vcard-temp' /></iq>");
  330. } else {
  331. $this->send("<iq type='get' id='$id'><vCard xmlns='vcard-temp' /></iq>");
  332. }
  333. }
  334. /**
  335. * Features handler
  336. *
  337. * @param XMLObj $xml
  338. * @throws Exception
  339. */
  340. protected function features_handler(XMLObj $xml): void
  341. {
  342. if ($xml->hasSub('starttls') and $this->use_encryption) {
  343. $this->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>");
  344. } elseif ($xml->hasSub('bind') and $this->authed) {
  345. $id = $this->getId();
  346. $this->addIdHandler($id, [$this, 'resource_bind_handler']);
  347. $this->send("<iq xmlns=\"jabber:client\" type=\"set\" id=\"$id\"><bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"><resource>{$this->resource}</resource></bind></iq>");
  348. } else {
  349. $this->log->log("Attempting Auth...");
  350. if ($this->password) {
  351. $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" . base64_encode("\x00" . $this->user . "\x00" . $this->password) . "</auth>");
  352. } else {
  353. $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>");
  354. }
  355. }
  356. }
  357. /**
  358. * SASL success handler
  359. *
  360. * @param XMLObj $xml
  361. * @throws Exception
  362. */
  363. protected function sasl_success_handler(XMLObj $xml): void
  364. {
  365. $this->log->log("Auth success!");
  366. $this->authed = true;
  367. $this->reset();
  368. }
  369. /**
  370. * SASL feature handler
  371. *
  372. * @param XMLObj $xml
  373. * @throws Exception
  374. */
  375. protected function sasl_failure_handler(XMLObj $xml): void
  376. {
  377. $this->log->log("Auth failed!", Log::LEVEL_ERROR);
  378. $this->disconnect();
  379. throw new Exception('Auth failed!');
  380. }
  381. /**
  382. * Resource bind handler
  383. *
  384. * @param XMLObj $xml
  385. * @throws Exception
  386. */
  387. protected function resource_bind_handler(XMLObj $xml): void
  388. {
  389. if ($xml->attrs['type'] == 'result') {
  390. $this->log->log("Bound to " . $xml->sub('bind')->sub('jid')->data);
  391. $this->fulljid = $xml->sub('bind')->sub('jid')->data;
  392. $jidarray = explode('/', $this->fulljid);
  393. $this->jid = $jidarray[0];
  394. }
  395. $id = $this->getId();
  396. $this->addIdHandler($id, [$this, 'session_start_handler']);
  397. $this->send("<iq xmlns='jabber:client' type='set' id='$id'><session xmlns='urn:ietf:params:xml:ns:xmpp-session' /></iq>");
  398. }
  399. /**
  400. * Roster iq handler
  401. * Gets all packets matching XPath "iq/{jabber:iq:roster}query'
  402. *
  403. * @param XMLObj $xml
  404. * @throws Exception
  405. */
  406. protected function roster_iq_handler(XMLObj $xml): void
  407. {
  408. $status = 'result';
  409. $xmlroster = $xml->sub('query');
  410. $contacts = [];
  411. foreach ($xmlroster->subs as $item) {
  412. $groups = [];
  413. if ($item->name === 'item') {
  414. $jid = $item->attrs['jid']; // REQUIRED
  415. $name = $item->attrs['name'] ?? '';
  416. $subscription = $item->attrs['subscription'] ?? 'none';
  417. foreach ($item->subs as $subitem) {
  418. if ($subitem->name === 'group') {
  419. $groups[] = $subitem->data;
  420. }
  421. }
  422. // Store for action if no errors happen
  423. $contacts[] = [$jid, $subscription, $name, $groups];
  424. } else {
  425. $status = 'error';
  426. }
  427. }
  428. // No errors, add contacts
  429. if ($status === 'result') {
  430. foreach ($contacts as $contact) {
  431. $this->roster->addContact(...$contact);
  432. }
  433. }
  434. if ($xml->attrs['type'] == 'set') {
  435. $this->send("<iq type=\"reply\" id=\"{$xml->attrs['id']}\" to=\"{$xml->attrs['from']}\" />");
  436. }
  437. }
  438. /**
  439. * Session start handler
  440. *
  441. * @param XMLObj $xml
  442. */
  443. protected function session_start_handler(XMLObj $xml): void
  444. {
  445. $this->log->log("Session started");
  446. $this->session_started = true;
  447. $this->event('session_start');
  448. }
  449. /**
  450. * TLS proceed handler
  451. *
  452. * @param XMLObj $xml
  453. * @throws Exception
  454. */
  455. protected function tls_proceed_handler(XMLObj $xml): void
  456. {
  457. $this->log->log("Starting TLS encryption");
  458. stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  459. $this->reset();
  460. }
  461. /**
  462. * VCard retrieval handler
  463. *
  464. * @param XMLObj $xml
  465. */
  466. protected function vcard_get_handler(XMLObj $xml): void
  467. {
  468. $vcard_array = [];
  469. $vcard = $xml->sub('vcard');
  470. // go through all of the sub elements and add them to the vcard array
  471. foreach ($vcard->subs as $sub) {
  472. if ($sub->subs) {
  473. $vcard_array[$sub->name] = [];
  474. foreach ($sub->subs as $sub_child) {
  475. $vcard_array[$sub->name][$sub_child->name] = $sub_child->data;
  476. }
  477. } else {
  478. $vcard_array[$sub->name] = $sub->data;
  479. }
  480. }
  481. $vcard_array['from'] = $xml->attrs['from'];
  482. $this->event('vcard', $vcard_array);
  483. }
  484. }