XMPP.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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('{http://etherx.jabber.org/streams}features', 'features_handler');
  129. $this->addXPathHandler('{urn:ietf:params:xml:ns:xmpp-sasl}success', 'sasl_success_handler');
  130. $this->addXPathHandler('{urn:ietf:params:xml:ns:xmpp-sasl}failure', 'sasl_failure_handler');
  131. $this->addXPathHandler('{urn:ietf:params:xml:ns:xmpp-tls}proceed', 'tls_proceed_handler');
  132. $this->addXPathHandler('{jabber:client}message', 'message_handler');
  133. $this->addXPathHandler('{jabber:client}presence', 'presence_handler');
  134. $this->addXPathHandler('iq/{jabber:iq:roster}query', 'roster_iq_handler');
  135. }
  136. /**
  137. * Turn encryption on/ff
  138. *
  139. * @param bool $useEncryption (optional)
  140. */
  141. public function useEncryption(bool $useEncryption = true): void
  142. {
  143. $this->use_encryption = $useEncryption;
  144. }
  145. /**
  146. * Turn on auto-authorization of subscription requests.
  147. *
  148. * @param bool $autoSubscribe (optional)
  149. */
  150. public function autoSubscribe(bool $autoSubscribe = true): void
  151. {
  152. $this->auto_subscribe = $autoSubscribe;
  153. }
  154. /**
  155. * Send XMPP Message
  156. *
  157. * @param string $to
  158. * @param string $body
  159. * @param string $type (optional)
  160. * @param string|null $subject (optional)
  161. * @param string|null $payload (optional)
  162. * @throws Exception
  163. */
  164. public function message(string $to, string $body, string $type = 'chat', ?string $subject = null, ?string $payload = null): void
  165. {
  166. if ($this->disconnected) {
  167. throw new Exception('You need to connect first');
  168. }
  169. if (empty($type)) {
  170. $type = 'chat';
  171. }
  172. $to = htmlspecialchars($to);
  173. $body = htmlspecialchars($body);
  174. $subject = htmlspecialchars($subject);
  175. $subject = ($subject) ? '<subject>' . $subject . '</subject>' : '';
  176. $payload = ($payload) ? $payload : '';
  177. $sprintf = '<message from="%s" to="%s" type="%s">%s<body>%s</body>%s</message>';
  178. $output = sprintf($sprintf, $this->fulljid, $to, $type, $subject, $body, $payload);
  179. $this->send($output);
  180. }
  181. /**
  182. * Set Presence
  183. *
  184. * @param string $status
  185. * @param string $show
  186. * @param string $to
  187. * @param string $type
  188. * @param null $priority
  189. * @throws Exception
  190. */
  191. public function presence($status = null, $show = 'available', $to = null, $type = 'available', $priority = null): void
  192. {
  193. if ($this->disconnected) {
  194. throw new Exception('You need to connect first');
  195. }
  196. if ($type == 'available') {
  197. $type = '';
  198. }
  199. $to = htmlspecialchars($to);
  200. $status = htmlspecialchars($status);
  201. if ($show == 'unavailable') {
  202. $type = 'unavailable';
  203. }
  204. $out = "<presence";
  205. if ($to) {
  206. $out .= " to=\"$to\"";
  207. }
  208. if ($type) {
  209. $out .= " type='$type'";
  210. }
  211. if ($show == 'available' and !$status and $priority !== null) {
  212. $out .= "/>";
  213. } else {
  214. $out .= ">";
  215. if ($show != 'available') {
  216. $out .= "<show>$show</show>";
  217. }
  218. if ($status) {
  219. $out .= "<status>$status</status>";
  220. }
  221. if ($priority !== null) {
  222. $out .= "<priority>$priority</priority>";
  223. }
  224. $out .= "</presence>";
  225. }
  226. $this->send($out);
  227. }
  228. /**
  229. * Send Auth request
  230. *
  231. * @param string $jid
  232. * @throws Exception
  233. */
  234. public function subscribe(string $jid): void
  235. {
  236. $this->send("<presence type='subscribe' to='{$jid}' from='{$this->fulljid}' />");
  237. #$this->send("<presence type='subscribed' to='{$jid}' from='{$this->fulljid}' />");
  238. }
  239. /**
  240. * Message handler
  241. *
  242. * @param XMLObj $xml
  243. */
  244. public function message_handler(XMLObj $xml): void
  245. {
  246. if (isset($xml->attrs['type'])) {
  247. $payload['type'] = $xml->attrs['type'];
  248. } else {
  249. $payload['type'] = 'chat';
  250. }
  251. $body = $xml->sub('body');
  252. $payload['from'] = $xml->attrs['from'];
  253. $payload['body'] = is_object($body) ? $body->data : false; // $xml->sub('body')->data;
  254. $payload['xml'] = $xml;
  255. $this->log->log("Message: {$payload['body']}", Log::LEVEL_DEBUG);
  256. $this->event('message', $payload);
  257. }
  258. /**
  259. * Presence handler
  260. *
  261. * @param XMLObj $xml
  262. * @throws Exception
  263. */
  264. public function presence_handler(XMLObj $xml): void
  265. {
  266. $payload['type'] = (isset($xml->attrs['type'])) ? $xml->attrs['type'] : 'available';
  267. $payload['show'] = (isset($xml->sub('show')->data)) ? $xml->sub('show')->data : $payload['type'];
  268. $payload['from'] = $xml->attrs['from'];
  269. $payload['status'] = (isset($xml->sub('status')->data)) ? $xml->sub('status')->data : '';
  270. $payload['priority'] = (isset($xml->sub('priority')->data)) ? intval($xml->sub('priority')->data) : 0;
  271. $payload['xml'] = $xml;
  272. if ($this->track_presence) {
  273. $this->roster->setPresence($payload['from'], $payload['priority'], $payload['show'], $payload['status']);
  274. }
  275. $this->log->log("Presence: {$payload['from']} [{$payload['show']}] {$payload['status']}", Log::LEVEL_DEBUG);
  276. if (array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribe') {
  277. if ($this->auto_subscribe) {
  278. $this->send("<presence type='subscribed' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
  279. $this->send("<presence type='subscribe' to='{$xml->attrs['from']}' from='{$this->fulljid}' />");
  280. }
  281. $this->event('subscription_requested', $payload);
  282. } elseif (array_key_exists('type', $xml->attrs) and $xml->attrs['type'] == 'subscribed') {
  283. $this->event('subscription_accepted', $payload);
  284. } else {
  285. $this->event('presence', $payload);
  286. }
  287. }
  288. /**
  289. * Retrieves the roster
  290. *
  291. * @throws Exception
  292. */
  293. public function getRoster(): void
  294. {
  295. $id = $this->getID();
  296. $this->send("<iq xmlns='jabber:client' type='get' id='$id'><query xmlns='jabber:iq:roster' /></iq>");
  297. }
  298. /**
  299. * Retrieves the vcard
  300. * @param string|null $jid
  301. * @throws Exception
  302. */
  303. public function getVCard(?string $jid = null): void
  304. {
  305. $id = $this->getID();
  306. $this->addIdHandler($id, 'vcard_get_handler');
  307. if ($jid) {
  308. $this->send("<iq type='get' id='$id' to='$jid'><vCard xmlns='vcard-temp' /></iq>");
  309. } else {
  310. $this->send("<iq type='get' id='$id'><vCard xmlns='vcard-temp' /></iq>");
  311. }
  312. }
  313. /**
  314. * Features handler
  315. *
  316. * @param XMLObj $xml
  317. * @throws Exception
  318. */
  319. protected function features_handler(XMLObj $xml): void
  320. {
  321. if ($xml->hasSub('starttls') and $this->use_encryption) {
  322. $this->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>");
  323. } elseif ($xml->hasSub('bind') and $this->authed) {
  324. $id = $this->getId();
  325. $this->addIdHandler($id, 'resource_bind_handler');
  326. $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>");
  327. } else {
  328. $this->log->log("Attempting Auth...");
  329. if ($this->password) {
  330. $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>" . base64_encode("\x00" . $this->user . "\x00" . $this->password) . "</auth>");
  331. } else {
  332. $this->send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>");
  333. }
  334. }
  335. }
  336. /**
  337. * SASL success handler
  338. *
  339. * @param XMLObj $xml
  340. * @throws Exception
  341. */
  342. protected function sasl_success_handler(XMLObj $xml): void
  343. {
  344. $this->log->log("Auth success!");
  345. $this->authed = true;
  346. $this->reset();
  347. }
  348. /**
  349. * SASL feature handler
  350. *
  351. * @param XMLObj $xml
  352. * @throws Exception
  353. */
  354. protected function sasl_failure_handler(XMLObj $xml): void
  355. {
  356. $this->log->log("Auth failed!", Log::LEVEL_ERROR);
  357. $this->disconnect();
  358. throw new Exception('Auth failed!');
  359. }
  360. /**
  361. * Resource bind handler
  362. *
  363. * @param XMLObj $xml
  364. * @throws Exception
  365. */
  366. protected function resource_bind_handler(XMLObj $xml): void
  367. {
  368. if ($xml->attrs['type'] == 'result') {
  369. $this->log->log("Bound to " . $xml->sub('bind')->sub('jid')->data);
  370. $this->fulljid = $xml->sub('bind')->sub('jid')->data;
  371. $jidarray = explode('/', $this->fulljid);
  372. $this->jid = $jidarray[0];
  373. }
  374. $id = $this->getId();
  375. $this->addIdHandler($id, 'session_start_handler');
  376. $this->send("<iq xmlns='jabber:client' type='set' id='$id'><session xmlns='urn:ietf:params:xml:ns:xmpp-session' /></iq>");
  377. }
  378. /**
  379. * Roster iq handler
  380. * Gets all packets matching XPath "iq/{jabber:iq:roster}query'
  381. *
  382. * @param XMLObj $xml
  383. * @throws Exception
  384. */
  385. protected function roster_iq_handler(XMLObj $xml): void
  386. {
  387. $status = "result";
  388. $xmlroster = $xml->sub('query');
  389. $contacts = [];
  390. foreach ($xmlroster->subs as $item) {
  391. $groups = [];
  392. if ($item->name == 'item') {
  393. $jid = $item->attrs['jid']; //REQUIRED
  394. $name = $item->attrs['name']; //MAY
  395. $subscription = $item->attrs['subscription'];
  396. foreach ($item->subs as $subitem) {
  397. if ($subitem->name == 'group') {
  398. $groups[] = $subitem->data;
  399. }
  400. }
  401. $contacts[] = [$jid, $subscription, $name, $groups]; //Store for action if no errors happen
  402. } else {
  403. $status = "error";
  404. }
  405. }
  406. if ($status == "result") { //No errors, add contacts
  407. foreach ($contacts as $contact) {
  408. $this->roster->addContact($contact[0], $contact[1], $contact[2], $contact[3]);
  409. }
  410. }
  411. if ($xml->attrs['type'] == 'set') {
  412. $this->send("<iq type=\"reply\" id=\"{$xml->attrs['id']}\" to=\"{$xml->attrs['from']}\" />");
  413. }
  414. }
  415. /**
  416. * Session start handler
  417. *
  418. * @param XMLObj $xml
  419. */
  420. protected function session_start_handler(XMLObj $xml): void
  421. {
  422. $this->log->log("Session started");
  423. $this->session_started = true;
  424. $this->event('session_start');
  425. }
  426. /**
  427. * TLS proceed handler
  428. *
  429. * @param XMLObj $xml
  430. * @throws Exception
  431. */
  432. protected function tls_proceed_handler(XMLObj $xml): void
  433. {
  434. $this->log->log("Starting TLS encryption");
  435. stream_socket_enable_crypto($this->socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  436. $this->reset();
  437. }
  438. /**
  439. * VCard retrieval handler
  440. *
  441. * @param XMLObj $xml
  442. */
  443. protected function vcard_get_handler(XMLObj $xml): void
  444. {
  445. $vcard_array = [];
  446. $vcard = $xml->sub('vcard');
  447. // go through all of the sub elements and add them to the vcard array
  448. foreach ($vcard->subs as $sub) {
  449. if ($sub->subs) {
  450. $vcard_array[$sub->name] = [];
  451. foreach ($sub->subs as $sub_child) {
  452. $vcard_array[$sub->name][$sub_child->name] = $sub_child->data;
  453. }
  454. } else {
  455. $vcard_array[$sub->name] = $sub->data;
  456. }
  457. }
  458. $vcard_array['from'] = $xml->attrs['from'];
  459. $this->event('vcard', $vcard_array);
  460. }
  461. }