]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Xmpp/XmppPlugin.php
Merge branch '1.0.x' into testing
[quix0rs-gnu-social.git] / plugins / Xmpp / XmppPlugin.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2009, StatusNet, Inc.
5  *
6  * Send and receive notices using the XMPP network
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  IM
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2009 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     // This check helps protect against security problems;
33     // your code file can't be executed directly from the web.
34     exit(1);
35 }
36
37 /**
38  * Plugin for XMPP
39  *
40  * @category  Plugin
41  * @package   StatusNet
42  * @author    Evan Prodromou <evan@status.net>
43  * @copyright 2009 StatusNet, Inc.
44  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
45  * @link      http://status.net/
46  */
47
48 class XmppPlugin extends ImPlugin
49 {
50     public $server = null;
51     public $port = 5222;
52     public $user =  'update';
53     public $resource = null;
54     public $encryption = true;
55     public $password = null;
56     public $host = null;  // only set if != server
57     public $debug = false; // print extra debug info
58
59     public $transport = 'xmpp';
60
61     function getDisplayName(){
62         return _m('XMPP/Jabber/GTalk');
63     }
64
65     /**
66      * Splits a Jabber ID (JID) into node, domain, and resource portions.
67      * 
68      * Based on validation routine submitted by:
69      * @copyright 2009 Patrick Georgi <patrick@georgi-clan.de>
70      * @license Licensed under ISC-L, which is compatible with everything else that keeps the copyright notice intact. 
71      *
72      * @param string $jid string to check
73      *
74      * @return array with "node", "domain", and "resource" indices
75      * @throws Exception if input is not valid
76      */
77
78     protected function splitJid($jid)
79     {
80         $chars = '';
81         /* the following definitions come from stringprep, Appendix C,
82            which is used in its entirety by nodeprop, Chapter 5, "Prohibited Output" */
83         /* C1.1 ASCII space characters */
84         $chars .= "\x{20}";
85         /* C1.2 Non-ASCII space characters */
86         $chars .= "\x{a0}\x{1680}\x{2000}-\x{200b}\x{202f}\x{205f}\x{3000a}";
87         /* C2.1 ASCII control characters */
88         $chars .= "\x{00}-\x{1f}\x{7f}";
89         /* C2.2 Non-ASCII control characters */
90         $chars .= "\x{80}-\x{9f}\x{6dd}\x{70f}\x{180e}\x{200c}\x{200d}\x{2028}\x{2029}\x{2060}-\x{2063}\x{206a}-\x{206f}\x{feff}\x{fff9}-\x{fffc}\x{1d173}-\x{1d17a}";
91         /* C3 - Private Use */
92         $chars .= "\x{e000}-\x{f8ff}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}";
93         /* C4 - Non-character code points */
94         $chars .= "\x{fdd0}-\x{fdef}\x{fffe}\x{ffff}\x{1fffe}\x{1ffff}\x{2fffe}\x{2ffff}\x{3fffe}\x{3ffff}\x{4fffe}\x{4ffff}\x{5fffe}\x{5ffff}\x{6fffe}\x{6ffff}\x{7fffe}\x{7ffff}\x{8fffe}\x{8ffff}\x{9fffe}\x{9ffff}\x{afffe}\x{affff}\x{bfffe}\x{bffff}\x{cfffe}\x{cffff}\x{dfffe}\x{dffff}\x{efffe}\x{effff}\x{ffffe}\x{fffff}\x{10fffe}\x{10ffff}";
95         /* C5 - Surrogate codes */
96         $chars .= "\x{d800}-\x{dfff}";
97         /* C6 - Inappropriate for plain text */
98         $chars .= "\x{fff9}-\x{fffd}";
99         /* C7 - Inappropriate for canonical representation */
100         $chars .= "\x{2ff0}-\x{2ffb}";
101         /* C8 - Change display properties or are deprecated */
102         $chars .= "\x{340}\x{341}\x{200e}\x{200f}\x{202a}-\x{202e}\x{206a}-\x{206f}";
103         /* C9 - Tagging characters */
104         $chars .= "\x{e0001}\x{e0020}-\x{e007f}";
105     
106         /* Nodeprep forbids some more characters */
107         $nodeprepchars = $chars;
108         $nodeprepchars .= "\x{22}\x{26}\x{27}\x{2f}\x{3a}\x{3c}\x{3e}\x{40}";
109     
110         $parts = explode("/", $jid, 2);
111         if (count($parts) > 1) {
112             $resource = $parts[1];
113             if ($resource == '') {
114                 // Warning: empty resource isn't legit.
115                 // But if we're normalizing, we may as well take it...
116             }
117         } else {
118             $resource = null;
119         }
120     
121         $node = explode("@", $parts[0]);
122         if ((count($node) > 2) || (count($node) == 0)) {
123             throw new Exception("Invalid JID: too many @s");
124         } else if (count($node) == 1) {
125             $domain = $node[0];
126             $node = null;
127         } else {
128             $domain = $node[1];
129             $node = $node[0];
130             if ($node == '') {
131                 throw new Exception("Invalid JID: @ but no node");
132             }
133         }
134     
135         // Length limits per http://xmpp.org/rfcs/rfc3920.html#addressing
136         if ($node !== null) {
137             if (strlen($node) > 1023) {
138                 throw new Exception("Invalid JID: node too long.");
139             }
140             if (preg_match("/[".$nodeprepchars."]/u", $node)) {
141                 throw new Exception("Invalid JID node '$node'");
142             }
143         }
144     
145         if (strlen($domain) > 1023) {
146             throw new Exception("Invalid JID: domain too long.");
147         }
148         if (!common_valid_domain($domain)) {
149             throw new Exception("Invalid JID domain name '$domain'");
150         }
151     
152         if ($resource !== null) {
153             if (strlen($resource) > 1023) {
154                 throw new Exception("Invalid JID: resource too long.");
155             }
156             if (preg_match("/[".$chars."]/u", $resource)) {
157                 throw new Exception("Invalid JID resource '$resource'");
158             }
159         }
160     
161         return array('node' => is_null($node) ? null : mb_strtolower($node),
162                      'domain' => is_null($domain) ? null : mb_strtolower($domain),
163                      'resource' => $resource);
164     }
165     
166     /**
167      * Checks whether a string is a syntactically valid Jabber ID (JID),
168      * either with or without a resource.
169      * 
170      * Note that a bare domain can be a valid JID.
171      * 
172      * @param string $jid string to check
173      * @param bool $check_domain whether we should validate that domain...
174      *
175      * @return     boolean whether the string is a valid JID
176      */
177     protected function validateFullJid($jid, $check_domain=false)
178     {
179         try {
180             $parts = $this->splitJid($jid);
181             if ($check_domain) {
182                 if (!$this->checkDomain($parts['domain'])) {
183                     return false;
184                 }
185             }
186             return $parts['resource'] !== ''; // missing or present; empty ain't kosher
187         } catch (Exception $e) {
188             return false;
189         }
190     }
191     
192     /**
193      * Checks whether a string is a syntactically valid base Jabber ID (JID).
194      * A base JID won't include a resource specifier on the end; since we
195      * take it off when reading input we can't really use them reliably
196      * to direct outgoing messages yet (sorry guys!)
197      * 
198      * Note that a bare domain can be a valid JID.
199      * 
200      * @param string $jid string to check
201      * @param bool $check_domain whether we should validate that domain...
202      *
203      * @return     boolean whether the string is a valid JID
204      */
205     protected function validateBaseJid($jid, $check_domain=false)
206     {
207         try {
208             $parts = $this->splitJid($jid);
209             if ($check_domain) {
210                 if (!$this->checkDomain($parts['domain'])) {
211                     return false;
212                 }
213             }
214             return ($parts['resource'] === null); // missing; empty ain't kosher
215         } catch (Exception $e) {
216             return false;
217         }
218     }
219
220     /**
221      * Normalizes a Jabber ID for comparison, dropping the resource component if any.
222      *
223      * @param string $jid JID to check
224      * @param bool $check_domain if true, reject if the domain isn't findable
225      *
226      * @return string an equivalent JID in normalized (lowercase) form
227      */
228
229     function normalize($jid)
230     {
231         try {
232             $parts = $this->splitJid($jid);
233             if ($parts['node'] !== null) {
234                 return $parts['node'] . '@' . $parts['domain'];
235             } else {
236                 return $parts['domain'];
237             }
238         } catch (Exception $e) {
239             return null;
240         }
241     }
242
243     /**
244      * Check if this domain's got some legit DNS record
245      */
246     protected function checkDomain($domain)
247     {
248         if (checkdnsrr("_xmpp-server._tcp." . $domain, "SRV")) {
249             return true;
250         }
251         if (checkdnsrr($domain, "ANY")) {
252             return true;
253         }
254         return false;
255     }
256
257     function daemonScreenname()
258     {
259         $ret = $this->user . '@' . $this->server;
260         if($this->resource)
261         {
262             return $ret . '/' . $this->resource;
263         }else{
264             return $ret;
265         }
266     }
267
268     function validate($screenname)
269     {
270         return $this->validateBaseJid($screenname, common_config('email', 'check_domain'));
271     }
272
273     /**
274      * Load related modules when needed
275      *
276      * @param string $cls Name of the class to be loaded
277      *
278      * @return boolean hook value; true means continue processing, false means stop.
279      */
280
281     function onAutoload($cls)
282     {
283         $dir = dirname(__FILE__);
284
285         switch ($cls)
286         {
287         case 'XMPPHP_XMPP':
288             require_once $dir . '/extlib/XMPPHP/XMPP.php';
289             return false;
290         case 'Sharing_XMPP':
291         case 'Queued_XMPP':
292             require_once $dir . '/'.$cls.'.php';
293             return false;
294         case 'XmppManager':
295             require_once $dir . '/'.strtolower($cls).'.php';
296             return false;
297         default:
298             return true;
299         }
300     }
301
302     function onStartImDaemonIoManagers(&$classes)
303     {
304         parent::onStartImDaemonIoManagers(&$classes);
305         $classes[] = new XmppManager($this); // handles pings/reconnects
306         return true;
307     }
308
309     function microiduri($screenname)
310     {
311         return 'xmpp:' . $screenname;    
312     }
313
314     function sendMessage($screenname, $body)
315     {
316         $this->queuedConnection()->message($screenname, $body, 'chat');
317     }
318
319     function sendNotice($screenname, $notice)
320     {
321         $msg   = $this->formatNotice($notice);
322         $entry = $this->format_entry($notice);
323         
324         $this->queuedConnection()->message($screenname, $msg, 'chat', null, $entry);
325         return true;
326     }
327
328     /**
329      * extra information for XMPP messages, as defined by Twitter
330      *
331      * @param Profile $profile Profile of the sending user
332      * @param Notice  $notice  Notice being sent
333      *
334      * @return string Extra information (Atom, HTML, addresses) in string format
335      */
336
337     function format_entry($notice)
338     {
339         $profile = $notice->getProfile();
340
341         $entry = $notice->asAtomEntry(true, true);
342
343         $xs = new XMLStringer();
344         $xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im'));
345         $xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml'));
346         $xs->element('a', array('href' => $profile->profileurl),
347                      $profile->nickname);
348         $xs->text(": ");
349         if (!empty($notice->rendered)) {
350             $xs->raw($notice->rendered);
351         } else {
352             $xs->raw(common_render_content($notice->content, $notice));
353         }
354         $xs->text(" ");
355         $xs->element('a', array(
356             'href'=>common_local_url('conversation',
357                 array('id' => $notice->conversation)).'#notice-'.$notice->id),
358              // TRANS: %s is a notice ID.
359              sprintf(_m('[%s]'),$notice->id));
360         $xs->elementEnd('body');
361         $xs->elementEnd('html');
362
363         $html = $xs->getString();
364
365         return $html . ' ' . $entry;
366     }
367
368     function receiveRawMessage($pl)
369     {
370         $from = $this->normalize($pl['from']);
371
372         if ($pl['type'] != 'chat') {
373             $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from: " . $pl['xml']->toString());
374             return;
375         }
376
377         if (mb_strlen($pl['body']) == 0) {
378             $this->log(LOG_WARNING, "Ignoring message with empty body from $from: "  . $pl['xml']->toString());
379             return;
380         }
381
382         $this->handleIncoming($from, $pl['body']);
383         
384         return true;
385     }
386
387     /**
388      * Build a queue-proxied XMPP interface object. Any outgoing messages
389      * will be run back through us for enqueing rather than sent directly.
390      * 
391      * @return Queued_XMPP
392      * @throws Exception if server settings are invalid.
393      */
394     function queuedConnection(){
395         if(!isset($this->server)){
396             throw new Exception("must specify a server");
397         }
398         if(!isset($this->port)){
399             throw new Exception("must specify a port");
400         }
401         if(!isset($this->user)){
402             throw new Exception("must specify a user");
403         }
404         if(!isset($this->password)){
405             throw new Exception("must specify a password");
406         }
407
408         return new Queued_XMPP($this, $this->host ?
409                                     $this->host :
410                                     $this->server,
411                                     $this->port,
412                                     $this->user,
413                                     $this->password,
414                                     $this->resource,
415                                     $this->server,
416                                     $this->debug ?
417                                     true : false,
418                                     $this->debug ?
419                                     XMPPHP_Log::LEVEL_VERBOSE :  null
420                                     );
421     }
422
423     function onPluginVersion(&$versions)
424     {
425         $versions[] = array('name' => 'XMPP',
426                             'version' => STATUSNET_VERSION,
427                             'author' => 'Craig Andrews, Evan Prodromou',
428                             'homepage' => 'http://status.net/wiki/Plugin:XMPP',
429                             'rawdescription' =>
430                             _m('The XMPP plugin allows users to send and receive notices over the XMPP/Jabber network.'));
431         return true;
432     }
433 }
434