file block_course_list.php was initially added on branch MOODLE_14_STABLE.
[moodle.git] / mod / chat / chatd.php
CommitLineData
34308732 1#!/usr/bin/php -q
8e7eec60 2<?php
3
5a60e822 4define('QUIRK_CHUNK_UPDATE', 0x0001);
5
a950790d 6echo "Moodle chat daemon v1.0 (\$Id$)\n\n";
8e7eec60 7
8/// Set up all the variables we need /////////////////////////////////////
9
10/// $CFG variables are now defined in database by chat/lib.php
11
12$_SERVER['PHP_SELF'] = "dummy";
13$_SERVER['SERVER_NAME'] = "dummy";
14
15include('../../config.php');
16include('lib.php');
17
18$_SERVER['SERVER_NAME'] = $CFG->chat_serverhost;
19$_SERVER['PHP_SELF'] = "http://$CFG->chat_serverhost:$CFG->chat_serverport/mod/chat/chatd.php";
20
21$safemode = ini_get('safe_mode');
22
23if(!empty($safemode)) {
24 die("Error: Cannot run with PHP safe_mode = On. Turn off safe_mode.\n");
25}
26
27@set_time_limit (0);
28set_magic_quotes_runtime(0);
29
30error_reporting(E_ALL);
31
32function chat_empty_connection() {
33 return array('sid' => NULL, 'handle' => NULL, 'ip' => NULL, 'port' => NULL, 'groupid' => NULL);
34}
35
36class ChatConnection {
e7d27884 37 // Chat-related info
38 var $sid = NULL;
39 var $type = NULL;
40 //var $groupid = NULL;
8e7eec60 41
e7d27884 42 // PHP-level info
43 var $handle = NULL;
8e7eec60 44
e7d27884 45 // TCP/IP
46 var $ip = NULL;
47 var $port = NULL;
8e7eec60 48
e7d27884 49 function ChatConnection($resource) {
50 $this->handle = $resource;
51 socket_getpeername($this->handle, &$this->ip, &$this->port);
52 }
8e7eec60 53}
54
55class ChatDaemon {
56 var $conn_ufo = array(); // Connections not identified yet
57 var $conn_side = array(); // Sessions with sidekicks waiting for the main connection to be processed
58 var $conn_half = array(); // Sessions that have valid connections but not all of them
59 var $conn_sets = array(); // Sessions with complete connection sets sets
60 var $sets_info = array(); // Keyed by sessionid exactly like conn_sets, one of these for each of those
61
62 var $message_queue = array(); // Holds messages that we haven't committed to the DB yet
63
82a524ef 64 function update_lastmessageping($sessionid, $time = NULL) {
65 // TODO: this can and should be written as a single UPDATE query
66 if(empty($this->sets_info[$sessionid])) {
67 trace('update_lastmessageping() called for an invalid SID: '.$sessionid, E_USER_WARNING);
68 return false;
69 }
70
71 $now = time();
72 if(empty($time)) {
73 $time = $now;
74 }
75
76 // We 'll be cheating a little, and NOT updating lastmessageping
77 // as often as we have to, so we can save on DB queries (imagine MANY users)
78 $this->sets_info[$sessionid]['chatuser']->lastmessageping = $time;
79
80 // This will set it just fine for bookkeeping purposes.
81 if($now - $this->sets_info[$sessionid]['lastinfocommit'] > $this->live_data_update_threshold) {
82 // commit to permanent storage
fbabbd23 83 // trace('Committing volatile lastmessageping for session '.$sessionid);
82a524ef 84 $this->sets_info[$sessionid]['lastinfocommit'] = $now;
85 update_record('chat_users', $this->sets_info[$sessionid]['chatuser']);
86 }
87 return true;
88 }
89
b5de723d 90 function get_user_window($sessionid) {
82a524ef 91
92 global $CFG, $THEME;
93
94 static $str;
95
96 $info = &$this->sets_info[$sessionid];
97 $oldlang = chat_language_override($info['lang']);
98
99 $timenow = time();
100
101 if (empty($str)) {
102 $str->idle = get_string("idle", "chat");
103 $str->beep = get_string("beep", "chat");
104 $str->day = get_string("day");
105 $str->days = get_string("days");
106 $str->hour = get_string("hour");
107 $str->hours = get_string("hours");
108 $str->min = get_string("min");
109 $str->mins = get_string("mins");
110 $str->sec = get_string("sec");
111 $str->secs = get_string("secs");
112 }
113
b5de723d 114 ob_start();
82a524ef 115 echo '<html><head>';
116 echo '<script language="JavaScript">';
f0232c84 117 echo "<!-- //hide\n";
82a524ef 118
119 echo 'function openpopup(url,name,options,fullscreen) {';
120 echo 'fullurl = "'.$CFG->wwwroot.'" + url;';
121 echo 'windowobj = window.open(fullurl,name,options);';
122 echo 'if (fullscreen) {';
123 echo ' windowobj.moveTo(0,0);';
124 echo ' windowobj.resizeTo(screen.availWidth,screen.availHeight); ';
125 echo '}';
126 echo 'windowobj.focus();';
127 echo 'return false;';
f0232c84 128 echo "}\n-->\n";
82a524ef 129 echo '</script></head><body style="font-face: serif;" bgcolor="'.$THEME->body.'">';
130
131 echo '<table style="width: 100%;"><tbody>';
132 if(empty($this->sets_info)) {
133 // No users
134 echo '<tr><td>&nbsp;</td></tr>';
135 }
136 else {
137 foreach ($this->sets_info as $usersid => $userinfo) {
138 $lastping = $timenow - $userinfo['chatuser']->lastmessageping;
dfd629d7 139 $popuppar = '\'/user/view.php?id='.$userinfo['user']->id.'&amp;course='.$userinfo['courseid'].'\',\'user'.$userinfo['chatuser']->id.'\',\'\'';
82a524ef 140 echo '<tr><td width="35">';
dfd629d7 141 echo '<a target="_new" onclick="return openpopup('.$popuppar.');" href="'.$CFG->wwwroot.'/user/view.php?id='.$userinfo['chatuser']->id.'&amp;course='.$userinfo['courseid'].'">';
82a524ef 142 print_user_picture($userinfo['user']->id, 0, $userinfo['user']->picture, false, false, false);
143 echo "</a></td><td valign=center>";
144 echo "<p><font size=1>";
145 echo fullname($userinfo['user'])."<br />";
e7d27884 146 echo "<font color=\"#888888\">$str->idle: ".format_time($lastping, $str)."</font> ";
147 echo '<a target="empty" href="http://'.$CFG->chat_serverhost.':'.$CFG->chat_serverport.'/?win=beep&beep='.$userinfo['user']->id.
148 '&chat_sid='.$sessionid.'&groupid='.$this->sets_info[$sessionid]['groupid'].'">'.$str->beep."</a>\n";
82a524ef 149 echo "</font></p>";
150 echo "<td></tr>";
151 }
152 }
153 echo '</tbody></table>';
b5de723d 154
fbabbd23 155 // About 2K of HTML comments to force browsers to render the HTML
f0232c84 156 // echo $GLOBALS['CHAT_DUMMY_DATA'];
fbabbd23 157
b5de723d 158 echo "</body>\n</html>\n";
82a524ef 159
160 chat_language_restore($oldlang);
b5de723d 161 return ob_get_clean();
162
163 }
164
8e7eec60 165 function new_ufo_id() {
166 static $id = 0;
167 if($id++ === 0x1000000) { // Cycling very very slowly to prevent overflow
168 $id = 0;
169 }
170 return $id;
171 }
172
173 function process_sidekicks($sessionid) {
174 if(empty($this->conn_side[$sessionid])) {
175 return true;
176 }
177 foreach($this->conn_side[$sessionid] as $sideid => $sidekick) {
e7d27884 178 // TODO: is this late-dispatch working correctly?
8e7eec60 179 $this->dispatch_sidekick($sidekick['handle'], $sidekick['type'], $sessionid, $sidekick['customdata']);
180 unset($this->conn_side[$sessionid][$sideid]);
181 }
182 return true;
183 }
184
185 function dispatch_sidekick($handle, $type, $sessionid, $customdata) {
186 global $CFG;
187
188 switch($type) {
e7d27884 189 case CHAT_SIDEKICK_BEEP:
190 // Incoming beep
191 $msg = &New stdClass;
192 $msg->chatid = $this->sets_info[$sessionid]['chatid'];
193 $msg->userid = $this->sets_info[$sessionid]['userid'];
194 $msg->groupid = $this->sets_info[$sessionid]['groupid'];
195 $msg->system = 0;
196 $msg->message = 'beep '.$customdata['beep'];
197 $msg->timestamp = time();
198
199 // Commit to DB
200 insert_record('chat_messages', $msg);
201
202 // OK, now push it out to all users
203 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']);
204
205 // Update that user's lastmessageping
206 $this->update_lastmessageping($sessionid, $msg->timestamp);
207
208 // We did our work, but before slamming the door on the poor browser
209 // show the courtesy of responding to the HTTP request. Otherwise, some
210 // browsers decide to get vengeance by flooding us with repeat requests.
211
212 $header = "HTTP/1.1 200 OK\n";
213 $header .= "Connection: close\n";
214 $header .= "Date: ".date('r')."\n";
215 $header .= "Server: Moodle\n";
216 $header .= "Content-Type: text/html\n";
217 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n";
218 $header .= "Cache-Control: no-cache, must-revalidate\n";
219 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n";
220 $header .= "\n";
221
222 // That's enough headers for one lousy dummy response
223 chat_socket_write($handle, $header);
224 // All done
225 break;
226
8e7eec60 227 case CHAT_SIDEKICK_USERS:
fbabbd23 228 // A request to paint a user window
b5de723d 229
230 $content = $this->get_user_window($sessionid);
231
232 $header = "HTTP/1.1 200 OK\n";
233 $header .= "Connection: close\n";
234 $header .= "Date: ".date('r')."\n";
235 $header .= "Server: Moodle\n";
236 $header .= "Content-Type: text/html\n";
237 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n";
238 $header .= "Cache-Control: no-cache, must-revalidate\n";
239 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n";
240 $header .= "Content-Length: ".strlen($content)."\n";
82a524ef 241 $header .= "Refresh: $CFG->chat_refresh_userlist; URL=http://$CFG->chat_serverhost:$CFG->chat_serverport/?win=users&".
b5de723d 242 "chat_sid=".$sessionid."&groupid=".$this->sets_info[$sessionid]['groupid']."\n";
243 $header .= "\n";
244
245 // That's enough headers for one lousy dummy response
82a524ef 246 trace('writing users http response to handle '.$handle);
b5de723d 247 chat_socket_write($handle, $header . $content);
248
82a524ef 249/*
250 $header = "HTTP/1.1 200 OK\n";
251 $header .= "Connection: close\n";
252 $header .= "Date: ".date('r')."\n";
253 $header .= "Server: Moodle\n";
254 $header .= "Content-Type: text/html\n";
255 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n";
256 $header .= "Cache-Control: no-cache, must-revalidate\n";
257 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n";
258 $header .= "\n";
259 trace('writing users http response to handle '.$handle);
260 chat_socket_write($handle, $header);
261*/
8e7eec60 262 break;
263 case CHAT_SIDEKICK_MESSAGE:
264 // Incoming message
dfd629d7 265
266 // Browser stupidity protection from duplicate messages:
267 $messageindex = intval($customdata['index']);
268
269 if($this->sets_info[$sessionid]['lastmessageindex'] >= $messageindex) {
270 // We have already broadcasted that!
271 trace('discarding message with stale index');
272 break;
273 }
274 else {
275 // Update our info
276 $this->sets_info[$sessionid]['lastmessageindex'] = $messageindex;
277 }
278
e7d27884 279 $msg = &New stdClass;
8e7eec60 280 $msg->chatid = $this->sets_info[$sessionid]['chatid'];
281 $msg->userid = $this->sets_info[$sessionid]['userid'];
282 $msg->groupid = $this->sets_info[$sessionid]['groupid'];
283 $msg->system = 0;
284 $msg->message = urldecode($customdata['message']); // have to undo the browser's encoding
285 $msg->timestamp = time();
286
287 if(empty($msg->message)) {
288 // Someone just hit ENTER, send them on their way
289 break;
290 }
291
b5de723d 292 // Commit to DB
293 insert_record('chat_messages', $msg);
8e7eec60 294
295 // OK, now push it out to all users
b5de723d 296 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']);
8e7eec60 297
298 // Update that user's lastmessageping
82a524ef 299 $this->update_lastmessageping($sessionid, $msg->timestamp);
8e7eec60 300
b5de723d 301 // We did our work, but before slamming the door on the poor browser
302 // show the courtesy of responding to the HTTP request. Otherwise, some
303 // browsers decide to get vengeance by flooding us with repeat requests.
304
305 $header = "HTTP/1.1 200 OK\n";
306 $header .= "Connection: close\n";
307 $header .= "Date: ".date('r')."\n";
308 $header .= "Server: Moodle\n";
309 $header .= "Content-Type: text/html\n";
310 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n";
311 $header .= "Cache-Control: no-cache, must-revalidate\n";
312 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n";
313 $header .= "\n";
314
315 // That's enough headers for one lousy dummy response
316 chat_socket_write($handle, $header);
317
8e7eec60 318 // All done
319 break;
320 }
321
322 socket_shutdown($handle);
323 socket_close($handle);
324 }
325
a8185f2e 326 function promote_final($sessionid, $groupid, $customdata) {
8e7eec60 327 if(isset($this->conn_sets[$sessionid])) {
328 trace('Set cannot be finalized: Session '.$sessionid.' is already active');
329 return false;
330 }
331
332 $chatuser = get_record('chat_users', 'sid', $sessionid);
333 if($chatuser === false) {
334 $this->dismiss_half($sessionid);
335 return false;
336 }
337 $chat = get_record('chat', 'id', $chatuser->chatid);
338 if($chat === false) {
339 $this->dismiss_half($sessionid);
340 return false;
341 }
b5de723d 342 $user = get_record('user', 'id', $chatuser->userid);
343 if($user === false) {
344 $this->dismiss_half($sessionid);
345 return false;
346 }
347 $course = get_record('course', 'id', $chat->course); {
348 if($course === false) {
349 $this->dismiss_half($sessionid);
350 return false;
351 }
352 }
8e7eec60 353
b5de723d 354 global $CHAT_HTMLHEAD_JS, $CFG;
355
356 // A really sad thing, to have to do this by hand.... :-(
357 $lang = NULL;
358 if(empty($lang) && !empty($course->lang)) {
359 $lang = $course->lang;
360 }
361 if(empty($lang) && !empty($user->lang)) {
362 $lang = $user->lang;
363 }
364 if(empty($lang)) {
365 $lang = $CFG->lang;
366 }
8e7eec60 367
368 $this->conn_sets[$sessionid] = $this->conn_half[$sessionid];
82a524ef 369
370 // This whole thing needs to be purged of redundant info, and the
371 // code base to follow suit. But AFTER development is done.
b5de723d 372 $this->sets_info[$sessionid] = array(
82a524ef 373 'lastinfocommit' => 0,
dfd629d7 374 'lastmessageindex' => 0,
82a524ef 375 'courseid' => $course->id,
376 'chatuser' => $chatuser,
b5de723d 377 'chatid' => $chatuser->chatid,
378 'user' => $user,
379 'userid' => $chatuser->userid,
380 'groupid' => $groupid,
5a60e822 381 'lang' => $lang,
a8185f2e 382 'quirks' => $customdata['quirks']
b5de723d 383 );
384
a8185f2e 385 trace('QUIRKS value for this connection is '.$customdata['quirks']);
5a60e822 386
8e7eec60 387 $this->dismiss_half($sessionid, false);
388 chat_socket_write($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], $CHAT_HTMLHEAD_JS);
e7d27884 389 trace('Connection accepted: '.$this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL].', SID: '.$sessionid.' UID: '.$chatuser->userid.' GID: '.intval($groupid));
8e7eec60 390
391 // Finally, broadcast the "entered the chat" message
392
e7d27884 393 $msg = &New stdClass;
8e7eec60 394 $msg->chatid = $chatuser->chatid;
395 $msg->userid = $chatuser->userid;
396 $msg->groupid = 0;
397 $msg->system = 1;
398 $msg->message = 'enter';
399 $msg->timestamp = time();
400
401 insert_record('chat_messages', $msg);
b5de723d 402 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']);
8e7eec60 403
404 return true;
405 }
406
407 function promote_ufo($handle, $type, $sessionid, $groupid, $customdata) {
408 if(empty($this->conn_ufo)) {
409 return false;
410 }
411 foreach($this->conn_ufo as $id => $ufo) {
e7d27884 412 if($ufo->handle == $handle) {
8e7eec60 413 // OK, got the id of the UFO, but what is it?
414
415 if($type & CHAT_SIDEKICK) {
416 // Is the main connection ready?
417 if(isset($this->conn_sets[$sessionid])) {
418 // Yes, so dispatch this sidekick now and be done with it
82a524ef 419 //trace('Dispatching sidekick immediately');
8e7eec60 420 $this->dispatch_sidekick($handle, $type, $sessionid, $customdata);
421 $this->dismiss_ufo($handle, false);
422 }
423 else {
424 // No, so put it in the waiting list
425 trace('sidekick waiting');
426 $this->conn_side[$sessionid][] = array('type' => $type, 'handle' => $handle, 'customdata' => $customdata);
427 }
428 return true;
429 }
430
431 // If it's not a sidekick, at this point it can only be da man
432
433 if($type & CHAT_CONNECTION) {
434 // This forces a new connection right now...
e7d27884 435 trace('Incoming connection from '.$ufo->ip.':'.$ufo->port);
8e7eec60 436
437 // Do we have such a connection active?
438 if(isset($this->conn_sets[$sessionid])) {
439 // Yes, so regrettably we cannot promote you
e7d27884 440 trace('Connection rejected: session '.$sessionid.' is already final');
8e7eec60 441 $this->dismiss_ufo($handle);
442 return false;
443 }
444
445 // Join this with what we may have already
446 $this->conn_half[$sessionid][$type] = $handle;
447
448 // Do the bookkeeping
a8185f2e 449 $this->promote_final($sessionid, $groupid, $customdata);
8e7eec60 450
451 // It's not an UFO anymore
452 $this->dismiss_ufo($handle, false);
453
454 // Dispatch waiting sidekicks
455 $this->process_sidekicks($sessionid);
456
457 return true;
458 }
8e7eec60 459 }
460 }
461 return false;
462 }
463
464 function dismiss_half($sessionid, $disconnect = true) {
465 if(!isset($this->conn_half[$sessionid])) {
466 return false;
467 }
468 if($disconnect) {
469 foreach($this->conn_half[$sessionid] as $handle) {
e7d27884 470 @socket_shutdown($handle);
471 @socket_close($handle);
8e7eec60 472 }
473 }
474 unset($this->conn_half[$sessionid]);
475 return true;
476 }
477
478 function dismiss_set($sessionid) {
e7d27884 479 if(!empty($this->conn_sets[$sessionid])) {
480 foreach($this->conn_sets[$sessionid] as $handle) {
481 // Since we want to dismiss this, don't generate any errors if it's dead already
482 @socket_shutdown($handle);
483 @socket_close($handle);
484 }
8e7eec60 485 }
486 unset($this->conn_sets[$sessionid]);
487 unset($this->sets_info[$sessionid]);
488 return true;
489 }
490
491
492 function dismiss_ufo($handle, $disconnect = true) {
493 if(empty($this->conn_ufo)) {
494 return false;
495 }
496 foreach($this->conn_ufo as $id => $ufo) {
e7d27884 497 if($ufo->handle == $handle) {
8e7eec60 498 unset($this->conn_ufo[$id]);
499 if($disconnect) {
500 chat_socket_write($handle, "You don't seem to be a valid client.\n");
501 socket_shutdown($handle);
502 socket_close($handle);
503 }
504 return true;
505 }
506 }
507 return false;
508 }
509
510 function conn_accept() {
511 $handle = @socket_accept($this->listen_socket);
512 if(!$handle) {
513 return false;
514 }
515
e7d27884 516 $newconn = &New ChatConnection($handle);
8e7eec60 517 $id = $this->new_ufo_id();
e7d27884 518 $this->conn_ufo[$id] = $newconn;
8e7eec60 519
e7d27884 520 //trace('UFO #'.$id.': connection from '.$newconn->ip.' on port '.$newconn->port.', '.$newconn->handle);
8e7eec60 521 }
522
523 function conn_activity_ufo (&$handles) {
524 $monitor = array();
525 if(!empty($this->conn_ufo)) {
526 foreach($this->conn_ufo as $ufoid => $ufo) {
e7d27884 527 $monitor[$ufoid] = $ufo->handle;
8e7eec60 528 }
529 }
530
531 if(empty($monitor)) {
532 $handles = array();
533 return 0;
534 }
535
536 $retval = socket_select($monitor, $a = NULL, $b = NULL, NULL);
537 $handles = $monitor;
538
539 return $retval;
540 }
541
b5de723d 542 function message_broadcast($message, $sender) {
8e7eec60 543 if(empty($this->conn_sets)) {
544 return true;
545 }
546
8e7eec60 547 foreach($this->sets_info as $sessionid => $info) {
548 // We need to get handles from users that are in the same chatroom, same group
549 if($info['chatid'] == $message->chatid &&
550 ($info['groupid'] == $message->groupid || $message->groupid == 0))
551 {
552
553 // Simply give them the message
82a524ef 554 $output = chat_format_message_manually($message, 0, $sender, $info['user'], $info['lang']);
555 trace('Delivering message "'.$output->text.'" to '.$this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL]);
8e7eec60 556
b5de723d 557 if($output->beep) {
558 chat_socket_write($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], '<embed src="'.$this->beepsoundsrc.'" autostart="true" hidden="true" />');
559 }
560
5a60e822 561 if($info['quirks'] & QUIRK_CHUNK_UPDATE) {
562 $output->html .= $GLOBALS['CHAT_DUMMY_DATA'];
563 $output->html .= $GLOBALS['CHAT_DUMMY_DATA'];
564 $output->html .= $GLOBALS['CHAT_DUMMY_DATA'];
565 }
f0232c84 566
b5de723d 567 if(!chat_socket_write($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], $output->html)) {
8e7eec60 568
569 // Send failed! We must now disconnect/forget about the user FIRST
570 // and THEN broadcast a message to all others... otherwise, infinite recursion.
571
572 delete_records('chat_users', 'sid', $sessionid);
e7d27884 573 $msg = &New stdClass;
8e7eec60 574 $msg->chatid = $info['chatid'];
575 $msg->userid = $info['userid'];
576 $msg->groupid = 0;
577 $msg->system = 1;
578 $msg->message = 'exit';
579 $msg->timestamp = time();
580
581 trace('Client socket write failed, destroying uid '.$info['userid'].' with SID '.$sessionid);
582 insert_record('chat_messages', $msg);
583
b5de723d 584 // *************************** IMPORTANT
585 //
586 // Kill him BEFORE broadcasting, otherwise we 'll get infinite recursion!
587 //
588 // **********************************************************************
589 $latesender = $this->sets_info[$sessionid]['user'];
8e7eec60 590 $this->dismiss_set($sessionid);
b5de723d 591 $this->message_broadcast($msg, $latesender);
8e7eec60 592 }
593 //trace('Sent to UID '.$this->sets_info[$sessionid]['userid'].': '.$message->text_);
594 }
595 }
596 }
597
598 function message_commit() {
599 }
600
601}
602
603// Connection telltale
604define('CHAT_CONNECTION', 0x10);
605// Connections: Incrementing sequence, 0x10 to 0x1f
606define('CHAT_CONNECTION_CHANNEL', 0x11);
607
608// Sidekick telltale
609define('CHAT_SIDEKICK', 0x20);
610// Sidekicks: Incrementing sequence, 0x21 to 0x2f
611define('CHAT_SIDEKICK_USERS', 0x21);
612define('CHAT_SIDEKICK_MESSAGE', 0x22);
e7d27884 613define('CHAT_SIDEKICK_BEEP', 0x23);
8e7eec60 614
615
616$DAEMON = New ChatDaemon;
617$DAEMON->socket_active = false;
618$DAEMON->trace_level = E_ALL;
e7d27884 619$DAEMON->socketserver_refresh = 20;
8e7eec60 620$DAEMON->can_daemonize = function_exists('pcntl_fork');
b5de723d 621$DAEMON->beepsoundsrc = $CFG->wwwroot.'/mod/chat/beep.wav';
34308732 622$DAEMON->live_data_update_threshold = 15;
8e7eec60 623
624/// Check the parameters //////////////////////////////////////////////////////
625
34308732 626 $param = empty($argv[1]) ? NULL : trim(strtolower($argv[1]));
8e7eec60 627
628 if (empty($param) || eregi('^(\-\-help|\-h)$', $param)) {
629 echo 'Starts the Moodle chat socket server on port '.$CFG->chat_serverport;
630 echo "\n\n";
631 echo "Usage: chatd.php [-h|--start]\n\n";
632 echo "Example:\n";
633 echo " chatd.php --start\n\n";
634 echo "Options:\n";
635 echo " --start Starts the daemon\n";
636 echo " -h, --help Show this help\n";
637 echo "\n";
638 die();
639 }
640
641
b5de723d 642$logfile = fopen('chatd.log', 'a+');
8e7eec60 643
644/// Try to set up all the sockets ////////////////////////////////////////////////
645
646trace('Setting up sockets');
647
648if (!function_exists('socket_set_option')) {
649 // PHP < 4.3
650 if (!function_exists('socket_setopt')) {
651 // No socket_setopt!
652 echo "Error: Neither socket_setopt() nor socket_set_option() exists.\n";
653 echo "Possibly PHP has not been compiled with --enable-sockets.\n\n";
654 die();
655 }
656 function socket_set_option($socket, $level, $name, $val) {
657 return socket_setopt($socket, $level, $name, $val);
658 }
659}
660
661// Creating socket
662
663if(false === ($DAEMON->listen_socket = socket_create(AF_INET, SOCK_STREAM, 0))) {
664 // Failed to create socket
665 $DAEMON->last_error = socket_last_error();
666 echo "Error: socket_create() failed: ". socket_strerror(socket_last_error($DAEMON->last_error)).' ['.$DAEMON->last_error."]\n";
667 die();
668}
669
670//socket_close($DAEMON->listen_socket);
671//die();
672
673if(!socket_bind($DAEMON->listen_socket, $CFG->chat_serverip, $CFG->chat_serverport)) {
674 // Failed to bind socket
675 $DAEMON->last_error = socket_last_error();
676 echo "Error: socket_bind() failed: ". socket_strerror(socket_last_error($DAEMON->last_error)).' ['.$DAEMON->last_error."]\n";
b5de723d 677
e7d27884 678 if($DAEMON->last_error != 98) {
679 die();
b5de723d 680 }
e7d27884 681
8e7eec60 682}
683if(!socket_listen($DAEMON->listen_socket, $CFG->chat_servermax)) {
684 // Failed to get socket to listen
685 $DAEMON->last_error = socket_last_error();
686 echo "Error: socket_listen() failed: ". socket_strerror(socket_last_error($DAEMON->last_error)).' ['.$DAEMON->last_error."]\n";
687 die();
688}
689
690// Socket has been initialized and is ready
691trace('Socket opened on port '.$CFG->chat_serverport);
692$DAEMON->socket_active = true;
693
694// [pj]: I really must have a good read on sockets. What exactly does this do?
695// http://www.unixguide.net/network/socketfaq/4.5.shtml is still not enlightening enough for me.
696socket_set_option($DAEMON->listen_socket, SOL_SOCKET, SO_REUSEADDR, 1);
697socket_set_nonblock($DAEMON->listen_socket);
698
699/// Sockets all set up! Now we loop and process incoming data.
700/*
701declare(ticks=1);
702
703$pid = pcntl_fork();
704if ($pid == -1) {
705 die("could not fork");
706} else if ($pid) {
707 exit(); // we are the parent
708} else {
709 // we are the child
710}
711
712// detatch from the controlling terminal
713if (!posix_setsid()) {
714 die("could not detach from terminal");
715}
716
717// setup signal handlers
718pcntl_signal(SIGTERM, "sig_handler");
719pcntl_signal(SIGHUP, "sig_handler");
720*/
721
722if($DAEMON->can_daemonize) {
723 trace('Unholy spirit possession: daemonizing');
724 $DAEMON->pid = pcntl_fork();
725 if($pid == -1) {
726 trace('Process fork failed, terminating');
727 die();
728 }
729 else if($pid) {
730 // We are the parent
731 trace('Successfully forked the daemon with PID '.$pid);
732 die();
733 }
734 else {
735 // We are the daemon! :P
736 }
737
738 // FROM NOW ON, IT'S THE DAEMON THAT'S RUNNING!
739
740 // Detach from controlling terminal
741 if(!posix_setsid()) {
742 trace('Could not detach daemon process from terminal!');
743 }
744}
745else {
746 // Cannot go demonic
747 trace('Unholy spirit possession failed: PHP is not compiled with --enable-pcntl');
748}
749
750trace('Started Moodle chatd on port '.$CFG->chat_serverport.', listening socket '.$DAEMON->listen_socket, E_USER_WARNING);
751
752while(true) {
753 $active = array();
754
755 // First of all, let's see if any of our UFOs has identified itself
756 if($DAEMON->conn_activity_ufo($active)) {
757 foreach($active as $handle) {
758 $read_socket = array($handle);
759 $changed = socket_select($read_socket, $write = NULL, $except = NULL, 0, 0);
760
761 if($changed > 0) {
762 // Let's see what it has to say
763
764 $data = socket_read($handle, 512);
765 if(empty($data)) {
766 continue;
767 }
768
e7d27884 769 if(!ereg('win=(chat|users|message|beep).*&chat_sid=([a-zA-Z0-9]*)&groupid=([0-9]*) HTTP', $data, $info)) {
8e7eec60 770 // Malformed data
771 trace('UFO with '.$handle.': Request with malformed data; connection closed', E_USER_WARNING);
772 $DAEMON->dismiss_ufo($handle);
773 continue;
774 }
775
776 $type = $info[1];
777 $sessionid = $info[2];
778 $groupid = $info[3];
779
780 $customdata = array();
781
782 switch($type) {
783 case 'chat':
784 $type = CHAT_CONNECTION_CHANNEL;
695a4bff 785 $customdata['quirks'] = 0;
786 if(strpos($data, 'Safari')) {
5a60e822 787 trace('Safari identified...', E_USER_WARNING);
695a4bff 788 $customdata['quirks'] += QUIRK_CHUNK_UPDATE;
5a60e822 789 }
8e7eec60 790 break;
791 case 'users':
792 $type = CHAT_SIDEKICK_USERS;
793 break;
e7d27884 794 case 'beep':
795 $type = CHAT_SIDEKICK_BEEP;
796 if(!ereg('beep=([^&]*)[& ]', $data, $info)) {
797 trace('Beep sidekick did not contain a valid userid', E_USER_WARNING);
798 $DAEMON->dismiss_ufo($handle);
799 continue;
800 }
801 else {
802 $customdata = array('beep' => intval($info[1]));
803 }
804 break;
8e7eec60 805 case 'message':
806 $type = CHAT_SIDEKICK_MESSAGE;
dfd629d7 807 if(!ereg('chat_message=([^&]*)[& ]chat_msgidnr=([^&]*)[& ]', $data, $info)) {
8e7eec60 808 trace('Message sidekick did not contain a valid message', E_USER_WARNING);
809 $DAEMON->dismiss_ufo($handle);
810 continue;
811 }
812 else {
dfd629d7 813 $customdata = array('message' => $info[1], 'index' => $info[2]);
8e7eec60 814 }
815 break;
816 default:
817 trace('UFO with '.$handle.': Request with unknown type; connection closed', E_USER_WARNING);
818 $DAEMON->dismiss_ufo($handle);
819 continue;
820 break;
821 }
822
823 // OK, now we know it's something good... promote it and pass it all the data it needs
824 $DAEMON->promote_ufo($handle, $type, $sessionid, $groupid, $customdata);
825 continue;
826 }
827 }
828 }
829
830 // Finally, accept new connections
831 $DAEMON->conn_accept();
832
833 usleep($DAEMON->socketserver_refresh);
834}
835
836@socket_shutdown($DAEMON->listen_socket, 0);
837die("\n\n-- terminated --\n");
838
839
840function trace($message, $level = E_USER_NOTICE) {
841 global $DAEMON, $logfile;
842
843 $date = date('[Y-m-d H:i:s] ');
844 $severity = '';
845
846 switch($level) {
847 case E_USER_WARNING: $severity = '*IMPORTANT* '; break;
848 case E_USER_ERROR: $severity = ' *CRITICAL* '; break;
849 }
850
851 $message = $date.$severity.$message."\n";
852
853 if ($DAEMON->trace_level & $level) {
854 if($level & E_USER_ERROR) {
855 fwrite(STDERR, $message);
856 }
857 fwrite(STDOUT, $message);
858 fwrite($logfile, $message);
859 fflush($logfile);
860 }
861 flush();
862}
863
864function chat_socket_write($connection, $text) {
865 $check_socket = array($connection);
866 $socket_changed = socket_select($read = NULL, $check_socket, $except = NULL, 0, 0);
867 if($socket_changed > 0) {
b5de723d 868 $written = socket_write($connection, $text, strlen($text));
869 //trace('socket_write wrote '.$written.' of '.strlen($text).' bytes');
8e7eec60 870 return true;
871 }
872 return false;
873}
874
8e7eec60 875
876?>