]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/xmppdaemon.php
csrf protection in userauthorization
[quix0rs-gnu-social.git] / scripts / xmppdaemon.php
1 #!/usr/bin/env php
2 <?php
3 /*
4  * Laconica - a distributed open-source microblogging tool
5  * Copyright (C) 2008, Controlez-Vous, Inc.
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program 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 Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20
21 function xmppdaemon_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
22     switch ($errno) {
23      case E_USER_ERROR:
24                 echo "ERROR: [$errno] $errstr ($errfile:$errline)\n";
25                 echo "  Fatal error on line $errline in file $errfile";
26                 echo ", PHP " . PHP_VERSION . " (" . PHP_OS . ")\n";
27                 echo "Aborting...\n";
28                 exit(1);
29                 break;
30
31          case E_USER_WARNING:
32                 echo "WARNING [$errno] $errstr ($errfile:$errline)\n";
33                 break;
34
35      case E_USER_NOTICE:
36                 echo "NOTICE [$errno] $errstr ($errfile:$errline)\n";
37                 break;
38     }
39
40     /* Don't execute PHP internal error handler */
41     return true;
42 }
43
44 set_error_handler('xmppdaemon_error_handler');
45
46 # Abort if called from a web server
47 if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
48         print "This script must be run from the command line\n";
49         exit();
50 }
51
52 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
53 define('LACONICA', true);
54 define('CLAIM_TIMEOUT', 100000);
55
56 define('MAX_BROADCAST_COUNT', 20);
57 define('MAX_CONFIRM_COUNT', 20);
58
59 require_once(INSTALLDIR . '/lib/common.php');
60 require_once(INSTALLDIR . '/lib/jabber.php');
61
62 # This is kind of clunky; we create a class to call the global functions
63 # in jabber.php, which create a new XMPP class. A more elegant (?) solution
64 # might be to use make this a subclass of XMPP.
65
66 class XMPPDaemon {
67
68         function XMPPDaemon($resource=NULL) {
69                 static $attrs = array('server', 'port', 'user', 'password', 'host');
70
71                 foreach ($attrs as $attr)
72                 {
73                         $this->$attr = common_config('xmpp', $attr);
74                 }
75
76                 if ($resource) {
77                         $this->resource = $resource;
78                 } else {
79                         $this->resource = common_config('xmpp', 'resource') . 'daemon';
80                 }
81
82                 $this->log(LOG_INFO, "{$this->user}@{$this->server}/{$this->resource}");
83         }
84
85         function connect() {
86
87                 $connect_to = ($this->host) ? $this->host : $this->server;
88
89                 $this->log(LOG_INFO, "Connecting to $connect_to on port $this->port");
90
91                 $this->conn = jabber_connect($this->resource);
92
93                 if (!$this->conn) {
94                         return false;
95                 }
96
97                 return !$this->conn->isDisconnected();
98         }
99
100         function handle() {
101
102                 $this->conn->addEventHandler('message', 'handle_message', $this);
103                 $this->conn->addEventHandler('presence', 'handle_presence', $this);
104                 $this->conn->addEventHandler('session_start', 'handle_session_start', $this);
105                 
106                 while(!$this->conn->isDisconnected()) {
107                         $this->conn->processTime(5);
108                         $this->broadcast_queue();
109                         $this->confirmation_queue();
110                 }
111         }
112
113         function handle_session_start(&$pl) {
114                 $this->conn->getRoster();
115                 $this->set_status("Send me a message to post a notice");
116         }
117         
118         function get_user($from) {
119                 $user = User::staticGet('jabber', jabber_normalize_jid($from));
120                 return $user;
121         }
122
123         function get_confirmation($from) {
124                 $confirm = new Confirm_address();
125                 $confirm->address = $from;
126                 $confirm->address_type = 'jabber';
127                 if ($confirm->find(TRUE)) {
128                         return $confirm;
129                 } else {
130                         return NULL;
131                 }
132         }
133
134         function handle_message(&$pl) {
135                 if ($pl['type'] != 'chat') {
136                         return;
137                 }
138                 if (mb_strlen($pl['body']) == 0) {
139                         return;
140                 }
141
142                 $from = jabber_normalize_jid($pl['from']);
143                 $user = $this->get_user($from);
144
145                 if (!$user) {
146                         $this->from_site($from, 'Unknown user; go to ' .
147                                                          common_local_url('imsettings') .
148                                                          ' to add your address to your account');
149                         $this->log(LOG_WARNING, 'Message from unknown user ' . $from);
150                         return;
151                 }
152                 if ($this->handle_command($user, $pl['body'])) {
153                         return;
154                 } else if ($this->is_autoreply($pl['body'])) {
155                         $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from);
156                         return;
157                 } else if ($this->is_otr($pl['body'])) {
158                         $this->log(LOG_INFO, 'Ignoring OTR from ' . $from);
159                         return;
160                 } else {
161                         $len = mb_strlen($pl['body']);
162                         if($len > 140) {
163                                 $this->from_site($from, 'Message too long - maximum is 140 characters, you sent ' . $len);
164                                 return;
165                         }
166                         $this->add_notice($user, $pl);
167                 }
168         }
169
170         function is_autoreply($txt) {
171                 if (preg_match('/[\[\(]?[Aa]uto-?[Rr]eply[\]\)]/', $txt)) {
172                         return true;
173                 } else {
174                         return false;
175                 }
176         }
177
178         function is_otr($txt) {
179                 if (preg_match('/^\?OTR/', $txt)) {
180                         return true;
181                 } else {
182                         return false;
183                 }
184         }
185
186         function from_site($address, $msg) {
187                 $text = '['.common_config('site', 'name') . '] ' . $msg;
188                 jabber_send_message($address, $text);
189         }
190
191         function handle_command($user, $body) {
192                 # XXX: localise
193                 $p=explode(' ',$body);
194                 if(count($p)>2)
195                         return false;
196                 switch($p[0]) {
197                  case 'help':
198                         if(count($p)!=1)
199                                 return false;
200                         $this->from_site($user->jabber, "Commands:\n on     - turn on notifications\n off    - turn off notifications\n help   - show this help \n sub - subscribe to user\n unsub - unsubscribe from user");
201                         return true;
202                  case 'on':
203                         if(count($p)!=1)
204                                 return false;
205                         $this->set_notify($user, true);
206                         $this->from_site($user->jabber, 'notifications on');
207                         return true;
208                  case 'off':
209                         if(count($p)!=1)
210                                 return false;
211                         $this->set_notify($user, false);
212                         $this->from_site($user->jabber, 'notifications off');
213                         return true;
214                  case 'sub':
215                         if(count($p)==1) {
216                                 $this->from_site($user->jabber, 'Specify the name of the user to subscribe to');
217                                 return true;
218                         }
219                         $result=subs_subscribe_user($user, $p[1]);
220                         if($result=='true')
221                                 $this->from_site($user->jabber, 'Subscribed to ' . $p[1]);
222                         else
223                                 $this->from_site($user->jabber, $result);
224                         return true;
225                  case 'unsub':
226                         if(count($p)==1) {
227                                 $this->from_site($user->jabber, 'Specify the name of the user to unsubscribe from');
228                                 return true;
229                         }
230                         $result=subs_unsubscribe_user($user, $p[1]);
231                         if($result=='true')
232                                 $this->from_site($user->jabber, 'Unsubscribed from ' . $p[1]);
233                         else
234                                 $this->from_site($user->jabber, $result);
235                         return true;
236                  default:
237                         return false;
238                 }
239         }
240
241         function set_notify(&$user, $notify) {
242                 $orig = clone($user);
243                 $user->jabbernotify = $notify;
244                 $result = $user->update($orig);
245                 if (!$result) {
246                         $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
247                         $this->log(LOG_ERR,
248                                            'Could not set notify flag to ' . $notify .
249                                            ' for user ' . common_log_objstring($user) .
250                                            ': ' . $last_error->message);
251                 } else {
252                         $this->log(LOG_INFO,
253                                            'User ' . $user->nickname . ' set notify flag to ' . $notify);
254                 }
255         }
256
257         function add_notice(&$user, &$pl) {
258                 $notice = Notice::saveNew($user->id, trim(mb_substr($pl['body'], 0, 140)), 'xmpp');
259                 if (is_string($notice)) {
260                         $this->log(LOG_ERR, $notice);
261                         return;
262                 }
263                 common_real_broadcast($notice);
264                 $this->log(LOG_INFO,
265                                    'Added notice ' . $notice->id . ' from user ' . $user->nickname);
266         }
267
268         function handle_presence(&$pl) {
269                 $from = jabber_normalize_jid($pl['from']);
270                 switch ($pl['type']) {
271                  case 'subscribe':
272                         # We let anyone subscribe
273                         $this->subscribed($from);
274                         $this->log(LOG_INFO,
275                                            'Accepted subscription from ' . $from);
276                         break;
277                  case 'subscribed':
278                  case 'unsubscribed':
279                  case 'unsubscribe':
280                         $this->log(LOG_INFO,
281                                            'Ignoring  "' . $pl['type'] . '" from ' . $from);
282                         break;
283                  default:
284                         if (!$pl['type']) {
285                                 $user = User::staticGet('jabber', $from);
286                                 if (!$user) {
287                                         $this->log(LOG_WARNING, 'Presence from unknown user ' . $from);
288                                         return;
289                                 }
290                                 if ($user->updatefrompresence) {
291                                         $this->log(LOG_INFO, 'Updating ' . $user->nickname .
292                                                            ' status from presence.');
293                                         $this->add_notice($user, $pl);
294                                 }
295                         }
296                         break;
297                 }
298         }
299
300         function log($level, $msg) {
301                 common_log($level, 'XMPPDaemon('.$this->resource.'): '.$msg);
302         }
303
304         function subscribed($to) {
305                 jabber_special_presence('subscribed', $to);
306         }
307
308         function set_status($status) {
309                 $this->log(LOG_INFO, 'Setting status to "' . $status . '"');
310                 jabber_send_presence($status);
311         }
312
313         function top_queue_item() {
314
315                 $qi = new Queue_item();
316                 $qi->orderBy('created');
317                 $qi->whereAdd('claimed is NULL');
318
319                 $qi->limit(1);
320
321                 $cnt = $qi->find(TRUE);
322
323                 if ($cnt) {
324                         # XXX: potential race condition
325                         # can we force it to only update if claimed is still NULL
326                         # (or old)?
327                         $this->log(LOG_INFO, 'claiming queue item = ' . $qi->notice_id);
328                         $orig = clone($qi);
329                         $qi->claimed = common_sql_now();
330                         $result = $qi->update($orig);
331                         if ($result) {
332                                 $this->log(LOG_INFO, 'claim succeeded.');
333                                 return $qi;
334                         } else {
335                                 $this->log(LOG_INFO, 'claim failed.');
336                         }
337                 }
338                 $qi = NULL;
339                 return NULL;
340         }
341
342         function broadcast_queue() {
343                 $this->clear_old_claims();
344                 $this->log(LOG_INFO, 'checking for queued notices');
345                 $cnt = 0;
346                 do {
347                         $qi = $this->top_queue_item();
348                         if ($qi) {
349                                 $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($qi->created));
350                                 $notice = Notice::staticGet($qi->notice_id);
351                                 if ($notice) {
352                                         $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id);
353                                         # XXX: what to do if broadcast fails?
354                                         $result = common_real_broadcast($notice, $this->is_remote($notice));
355                                         if (!$result) {
356                                                 $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id);
357                                                 $orig = $qi;
358                                                 $qi->claimed = NULL;
359                                                 $qi->update($orig);
360                                                 $this->log(LOG_WARNING, 'Abandoned claim for notice ID = ' . $notice->id);
361                                                 continue;
362                                         }
363                                         $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id);
364                                         $notice = NULL;
365                                 } else {
366                                         $this->log(LOG_WARNING, 'queue item for notice that does not exist');
367                                 }
368                                 $qi->delete();
369                                 $cnt++;
370                         }
371                 } while ($qi && $cnt < MAX_BROADCAST_COUNT);
372         }
373
374         function clear_old_claims() {
375                 $qi = new Queue_item();
376                 $qi->claimed = NULL;
377                 $qi->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
378                 $qi->update(DB_DATAOBJECT_WHEREADD_ONLY);
379         }
380
381         function is_remote($notice) {
382                 $user = User::staticGet($notice->profile_id);
383                 return !$user;
384         }
385
386         function confirmation_queue() {
387             # $this->clear_old_confirm_claims();
388                 $this->log(LOG_INFO, 'checking for queued confirmations');
389                 $cnt = 0;
390                 do {
391                         $confirm = $this->next_confirm();
392                         if ($confirm) {
393                                 $this->log(LOG_INFO, 'Sending confirmation for ' . $confirm->address);
394                                 $user = User::staticGet($confirm->user_id);
395                                 if (!$user) {
396                                         $this->log(LOG_WARNING, 'Confirmation for unknown user ' . $confirm->user_id);
397                                         continue;
398                                 }
399                                 $success = jabber_confirm_address($confirm->code,
400                                                                   $user->nickname,
401                                                                   $confirm->address);
402                                 if (!$success) {
403                                         $this->log(LOG_ERR, 'Confirmation failed for ' . $confirm->address);
404                                         # Just let the claim age out; hopefully things work then
405                                         continue;
406                                 } else {
407                                         $this->log(LOG_INFO, 'Confirmation sent for ' . $confirm->address);
408                                         # Mark confirmation sent
409                                         $original = clone($confirm);
410                                         $confirm->sent = $confirm->claimed;
411                                         $result = $confirm->update($original);
412                                         if (!$result) {
413                                                 $this->log(LOG_ERR, 'Cannot mark sent for ' . $confirm->address);
414                                                 # Just let the claim age out; hopefully things work then
415                                                 continue;
416                                         }
417                                 }
418                                 $cnt++;
419                         }
420                 } while ($confirm && $cnt < MAX_CONFIRM_COUNT);
421         }
422
423         function next_confirm() {
424                 $confirm = new Confirm_address();
425                 $confirm->whereAdd('claimed IS NULL');
426                 $confirm->whereAdd('sent IS NULL');
427                 # XXX: eventually we could do other confirmations in the queue, too
428                 $confirm->address_type = 'jabber';
429                 $confirm->orderBy('modified DESC');
430                 $confirm->limit(1);
431                 if ($confirm->find(TRUE)) {
432                         $this->log(LOG_INFO, 'Claiming confirmation for ' . $confirm->address);
433                         # working around some weird DB_DataObject behaviour
434                         $confirm->whereAdd(''); # clears where stuff
435                         $original = clone($confirm);
436                         $confirm->claimed = common_sql_now();
437                         $result = $confirm->update($original);
438                         if ($result) {
439                                 $this->log(LOG_INFO, 'Succeeded in claim! '. $result);
440                                 return $confirm;
441                         } else {
442                                 $this->log(LOG_INFO, 'Failed in claim!');
443                                 return false;
444                         }
445                 }
446                 return NULL;
447         }
448
449         function clear_old_confirm_claims() {
450                 $confirm = new Confirm();
451                 $confirm->claimed = NULL;
452                 $confirm->whereAdd('now() - claimed > '.CLAIM_TIMEOUT);
453                 $confirm->update(DB_DATAOBJECT_WHEREADD_ONLY);
454         }
455
456 }
457
458 mb_internal_encoding('UTF-8');
459
460 $resource = ($argc > 1) ? $argv[1] : NULL;
461
462 $daemon = new XMPPDaemon($resource);
463
464 if ($daemon->connect()) {
465         $daemon->set_status("Send me a message to post a notice");
466         $daemon->handle();
467 }
468
469 ?>