|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 #!/usr/bin/php -q 00002 <?php 00003 00004 // Browser quirks 00005 define('QUIRK_CHUNK_UPDATE', 0x0001); 00006 00007 // Connection telltale 00008 define('CHAT_CONNECTION', 0x10); 00009 // Connections: Incrementing sequence, 0x10 to 0x1f 00010 define('CHAT_CONNECTION_CHANNEL', 0x11); 00011 00012 // Sidekick telltale 00013 define('CHAT_SIDEKICK', 0x20); 00014 // Sidekicks: Incrementing sequence, 0x21 to 0x2f 00015 define('CHAT_SIDEKICK_USERS', 0x21); 00016 define('CHAT_SIDEKICK_MESSAGE', 0x22); 00017 define('CHAT_SIDEKICK_BEEP', 0x23); 00018 00019 $phpversion = phpversion(); 00020 echo 'Moodle chat daemon v1.0 on PHP '.$phpversion."\n\n"; 00021 00023 00025 00026 $_SERVER['PHP_SELF'] = 'dummy'; 00027 $_SERVER['SERVER_NAME'] = 'dummy'; 00028 $_SERVER['HTTP_USER_AGENT'] = 'dummy'; 00029 00030 define('NO_MOODLE_COOKIES', true); // session not used here 00031 00032 include('../../config.php'); 00033 include('lib.php'); 00034 00035 $_SERVER['SERVER_NAME'] = $CFG->chat_serverhost; 00036 $_SERVER['PHP_SELF'] = "http://$CFG->chat_serverhost:$CFG->chat_serverport/mod/chat/chatd.php"; 00037 00038 $safemode = ini_get('safe_mode'); 00039 00040 if($phpversion < '4.3') { 00041 die("Error: The Moodle chat daemon requires at least PHP version 4.3 to run.\n Since your version is $phpversion, you have to upgrade.\n\n"); 00042 } 00043 if(!empty($safemode)) { 00044 die("Error: Cannot run with PHP safe_mode = On. Turn off safe_mode in php.ini.\n"); 00045 } 00046 00047 $passref = ini_get('allow_call_time_pass_reference'); 00048 if(empty($passref)) { 00049 die("Error: Cannot run with PHP allow_call_time_pass_reference = Off. Turn on allow_call_time_pass_reference in php.ini.\n"); 00050 } 00051 00052 @set_time_limit (0); 00053 set_magic_quotes_runtime(0); 00054 error_reporting(E_ALL); 00055 00056 function chat_empty_connection() { 00057 return array('sid' => NULL, 'handle' => NULL, 'ip' => NULL, 'port' => NULL, 'groupid' => NULL); 00058 } 00059 00060 class ChatConnection { 00061 // Chat-related info 00062 var $sid = NULL; 00063 var $type = NULL; 00064 //var $groupid = NULL; 00065 00066 // PHP-level info 00067 var $handle = NULL; 00068 00069 // TCP/IP 00070 var $ip = NULL; 00071 var $port = NULL; 00072 00073 function ChatConnection($resource) { 00074 $this->handle = $resource; 00075 @socket_getpeername($this->handle, $this->ip, $this->port); 00076 } 00077 } 00078 00079 class ChatDaemon { 00080 var $_resetsocket = false; 00081 var $_readytogo = false; 00082 var $_logfile = false; 00083 var $_trace_to_console = true; 00084 var $_trace_to_stdout = true; 00085 var $_logfile_name = 'chatd.log'; 00086 var $_last_idle_poll = 0; 00087 00088 var $conn_ufo = array(); // Connections not identified yet 00089 var $conn_side = array(); // Sessions with sidekicks waiting for the main connection to be processed 00090 var $conn_half = array(); // Sessions that have valid connections but not all of them 00091 var $conn_sets = array(); // Sessions with complete connection sets sets 00092 var $sets_info = array(); // Keyed by sessionid exactly like conn_sets, one of these for each of those 00093 var $chatrooms = array(); // Keyed by chatid, holding arrays of data 00094 00095 // IMPORTANT: $conn_sets, $sets_info and $chatrooms must remain synchronized! 00096 // Pay extra attention when you write code that affects any of them! 00097 00098 function ChatDaemon() { 00099 $this->_trace_level = E_ALL ^ E_USER_NOTICE; 00100 $this->_pcntl_exists = function_exists('pcntl_fork'); 00101 $this->_time_rest_socket = 20; 00102 $this->_beepsoundsrc = $GLOBALS['CFG']->wwwroot.'/mod/chat/beep.wav'; 00103 $this->_freq_update_records = 20; 00104 $this->_freq_poll_idle_chat = $GLOBALS['CFG']->chat_old_ping; 00105 $this->_stdout = fopen('php://stdout', 'w'); 00106 if($this->_stdout) { 00107 // Avoid double traces for everything 00108 $this->_trace_to_console = false; 00109 } 00110 } 00111 00112 function error_handler ($errno, $errmsg, $filename, $linenum, $vars) { 00113 // Checks if an error needs to be suppressed due to @ 00114 if(error_reporting() != 0) { 00115 $this->trace($errmsg.' on line '.$linenum, $errno); 00116 } 00117 return true; 00118 } 00119 00120 function poll_idle_chats($now) { 00121 $this->trace('Polling chats to detect disconnected users'); 00122 if(!empty($this->chatrooms)) { 00123 foreach($this->chatrooms as $chatid => $chatroom) { 00124 if(!empty($chatroom['users'])) { 00125 foreach($chatroom['users'] as $sessionid => $userid) { 00126 // We will be polling each user as required 00127 $this->trace('...shall we poll '.$sessionid.'?'); 00128 if($this->sets_info[$sessionid]['chatuser']->lastmessageping < $this->_last_idle_poll) { 00129 $this->trace('YES!'); 00130 // This user hasn't been polled since his last message 00131 if($this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], '<!-- poll -->') === false) { 00132 // User appears to have disconnected 00133 $this->disconnect_session($sessionid); 00134 } 00135 } 00136 } 00137 } 00138 } 00139 } 00140 $this->_last_idle_poll = $now; 00141 } 00142 00143 function query_start() { 00144 return $this->_readytogo; 00145 } 00146 00147 function trace($message, $level = E_USER_NOTICE) { 00148 $severity = ''; 00149 00150 switch($level) { 00151 case E_USER_WARNING: $severity = '*IMPORTANT* '; break; 00152 case E_USER_ERROR: $severity = ' *CRITICAL* '; break; 00153 case E_NOTICE: 00154 case E_WARNING: $severity = ' *CRITICAL* [php] '; break; 00155 } 00156 00157 $date = date('[Y-m-d H:i:s] '); 00158 $message = $date.$severity.$message."\n"; 00159 00160 if ($this->_trace_level & $level) { 00161 // It is accepted for output 00162 00163 // Error-class traces go to STDERR too 00164 if($level & E_USER_ERROR) { 00165 fwrite(STDERR, $message); 00166 } 00167 00168 // Emit the message to wherever we should 00169 if($this->_trace_to_stdout) { 00170 fwrite($this->_stdout, $message); 00171 fflush($this->_stdout); 00172 } 00173 if($this->_trace_to_console) { 00174 echo $message; 00175 flush(); 00176 } 00177 if($this->_logfile) { 00178 fwrite($this->_logfile, $message); 00179 fflush($this->_logfile); 00180 } 00181 } 00182 } 00183 00184 function write_data($connection, $text) { 00185 $written = @socket_write($connection, $text, strlen($text)); 00186 if($written === false) { 00187 // $this->trace("socket_write() failed: reason: " . socket_strerror(socket_last_error($connection))); 00188 return false; 00189 } 00190 return true; 00191 00192 // Enclosing the above code inside this blocks makes sure that 00193 // "a socket write operation will not block". I 'm not so sure 00194 // if this is needed, as we have a nonblocking socket anyway. 00195 // If trouble starts to creep up, we 'll restore this. 00196 // $check_socket = array($connection); 00197 // $socket_changed = socket_select($read = NULL, $check_socket, $except = NULL, 0, 0); 00198 // if($socket_changed > 0) { 00199 // 00200 // // ABOVE CODE GOES HERE 00201 // 00202 // } 00203 // return false; 00204 } 00205 00206 function user_lazy_update($sessionid) { 00207 global $DB; 00208 00209 // TODO: this can and should be written as a single UPDATE query 00210 if(empty($this->sets_info[$sessionid])) { 00211 $this->trace('user_lazy_update() called for an invalid SID: '.$sessionid, E_USER_WARNING); 00212 return false; 00213 } 00214 00215 $now = time(); 00216 00217 // We 'll be cheating a little, and NOT updating the record data as 00218 // often as we can, so that we save on DB queries (imagine MANY users) 00219 if($now - $this->sets_info[$sessionid]['lastinfocommit'] > $this->_freq_update_records) { 00220 // commit to permanent storage 00221 $this->sets_info[$sessionid]['lastinfocommit'] = $now; 00222 $DB->update_record('chat_users', $this->sets_info[$sessionid]['chatuser']); 00223 } 00224 return true; 00225 } 00226 00227 function get_user_window($sessionid) { 00228 global $CFG, $PAGE, $OUTPUT; 00229 00230 static $str; 00231 00232 $info = &$this->sets_info[$sessionid]; 00233 $PAGE->set_course($info['course']); 00234 00235 $timenow = time(); 00236 00237 if (empty($str)) { 00238 $str->idle = get_string("idle", "chat"); 00239 $str->beep = get_string("beep", "chat"); 00240 $str->day = get_string("day"); 00241 $str->days = get_string("days"); 00242 $str->hour = get_string("hour"); 00243 $str->hours = get_string("hours"); 00244 $str->min = get_string("min"); 00245 $str->mins = get_string("mins"); 00246 $str->sec = get_string("sec"); 00247 $str->secs = get_string("secs"); 00248 $str->years = get_string('years'); 00249 } 00250 00251 ob_start(); 00252 $refresh_inval = $CFG->chat_refresh_userlist * 1000; 00253 echo <<<EOD 00254 <html><head> 00255 <meta http-equiv="refresh" content="$refresh_inval"> 00256 <style type="text/css"> img{border:0} </style> 00257 <script type="text/javascript"> 00258 //<![CDATA[ 00259 function openpopup(url,name,options,fullscreen) { 00260 fullurl = "$CFG->wwwroot" + url; 00261 windowobj = window.open(fullurl,name,options); 00262 if (fullscreen) { 00263 windowobj.moveTo(0,0); 00264 windowobj.resizeTo(screen.availWidth,screen.availHeight); 00265 } 00266 windowobj.focus(); 00267 return false; 00268 } 00269 //]]> 00270 </script></head><body><table><tbody> 00271 EOD; 00272 00273 // Get the users from that chatroom 00274 $users = $this->chatrooms[$info['chatid']]['users']; 00275 00276 foreach ($users as $usersessionid => $userid) { 00277 // Fetch each user's sessionid and then the rest of his data from $this->sets_info 00278 $userinfo = $this->sets_info[$usersessionid]; 00279 00280 $lastping = $timenow - $userinfo['chatuser']->lastmessageping; 00281 00282 echo '<tr><td width="35">'; 00283 00284 $link = '/user/view.php?id='.$userinfo['user']->id.'&course='.$userinfo['courseid']; 00285 $anchortagcontents = $OUTPUT->user_picture($userinfo['user'], array('courseid'=>$userinfo['courseid'])); 00286 00287 $action = new popup_action('click', $link, 'user'.$userinfo['chatuser']->id); 00288 $anchortag = $OUTPUT->action_link($link, $anchortagcontents, $action); 00289 00290 echo $anchortag; 00291 echo "</td><td valign=\"center\">"; 00292 echo "<p><font size=\"1\">"; 00293 echo fullname($userinfo['user'])."<br />"; 00294 echo "<font color=\"#888888\">$str->idle: ".format_time($lastping, $str)."</font> "; 00295 echo '<a target="empty" href="http://'.$CFG->chat_serverhost.':'.$CFG->chat_serverport.'/?win=beep&beep='.$userinfo['user']->id. 00296 '&chat_sid='.$sessionid.'">'.$str->beep."</a>\n"; 00297 echo "</font></p>"; 00298 echo "<td></tr>"; 00299 } 00300 00301 echo '</tbody></table>'; 00302 00303 // About 2K of HTML comments to force browsers to render the HTML 00304 // echo $GLOBALS['CHAT_DUMMY_DATA']; 00305 00306 echo "</body>\n</html>\n"; 00307 00308 return ob_get_clean(); 00309 00310 } 00311 00312 function new_ufo_id() { 00313 static $id = 0; 00314 if($id++ === 0x1000000) { // Cycling very very slowly to prevent overflow 00315 $id = 0; 00316 } 00317 return $id; 00318 } 00319 00320 function process_sidekicks($sessionid) { 00321 if(empty($this->conn_side[$sessionid])) { 00322 return true; 00323 } 00324 foreach($this->conn_side[$sessionid] as $sideid => $sidekick) { 00325 // TODO: is this late-dispatch working correctly? 00326 $this->dispatch_sidekick($sidekick['handle'], $sidekick['type'], $sessionid, $sidekick['customdata']); 00327 unset($this->conn_side[$sessionid][$sideid]); 00328 } 00329 return true; 00330 } 00331 00332 function dispatch_sidekick($handle, $type, $sessionid, $customdata) { 00333 global $CFG, $DB; 00334 00335 switch($type) { 00336 case CHAT_SIDEKICK_BEEP: 00337 // Incoming beep 00338 $msg = New stdClass; 00339 $msg->chatid = $this->sets_info[$sessionid]['chatid']; 00340 $msg->userid = $this->sets_info[$sessionid]['userid']; 00341 $msg->groupid = $this->sets_info[$sessionid]['groupid']; 00342 $msg->system = 0; 00343 $msg->message = 'beep '.$customdata['beep']; 00344 $msg->timestamp = time(); 00345 00346 // Commit to DB 00347 $DB->insert_record('chat_messages', $msg, false); 00348 $DB->insert_record('chat_messages_current', $msg, false); 00349 00350 // OK, now push it out to all users 00351 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']); 00352 00353 // Update that user's lastmessageping 00354 $this->sets_info[$sessionid]['chatuser']->lastping = $msg->timestamp; 00355 $this->sets_info[$sessionid]['chatuser']->lastmessageping = $msg->timestamp; 00356 $this->user_lazy_update($sessionid); 00357 00358 // We did our work, but before slamming the door on the poor browser 00359 // show the courtesy of responding to the HTTP request. Otherwise, some 00360 // browsers decide to get vengeance by flooding us with repeat requests. 00361 00362 $header = "HTTP/1.1 200 OK\n"; 00363 $header .= "Connection: close\n"; 00364 $header .= "Date: ".date('r')."\n"; 00365 $header .= "Server: Moodle\n"; 00366 $header .= "Content-Type: text/html; charset=utf-8\n"; 00367 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 00368 $header .= "Cache-Control: no-cache, must-revalidate\n"; 00369 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 00370 $header .= "\n"; 00371 00372 // That's enough headers for one lousy dummy response 00373 $this->write_data($handle, $header); 00374 // All done 00375 break; 00376 00377 case CHAT_SIDEKICK_USERS: 00378 // A request to paint a user window 00379 00380 $content = $this->get_user_window($sessionid); 00381 00382 $header = "HTTP/1.1 200 OK\n"; 00383 $header .= "Connection: close\n"; 00384 $header .= "Date: ".date('r')."\n"; 00385 $header .= "Server: Moodle\n"; 00386 $header .= "Content-Type: text/html; charset=utf-8\n"; 00387 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 00388 $header .= "Cache-Control: no-cache, must-revalidate\n"; 00389 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 00390 $header .= "Content-Length: ".strlen($content)."\n"; 00391 00392 // The refresh value is 2 seconds higher than the configuration variable because we are doing JS refreshes all the time. 00393 // However, if the JS doesn't work for some reason, we still want to refresh once in a while. 00394 $header .= "Refresh: ".(intval($CFG->chat_refresh_userlist) + 2)."; url=http://$CFG->chat_serverhost:$CFG->chat_serverport/?win=users&". 00395 "chat_sid=".$sessionid."\n"; 00396 $header .= "\n"; 00397 00398 // That's enough headers for one lousy dummy response 00399 $this->trace('writing users http response to handle '.$handle); 00400 $this->write_data($handle, $header . $content); 00401 00402 // Update that user's lastping 00403 $this->sets_info[$sessionid]['chatuser']->lastping = time(); 00404 $this->user_lazy_update($sessionid); 00405 00406 break; 00407 00408 case CHAT_SIDEKICK_MESSAGE: 00409 // Incoming message 00410 00411 // Browser stupidity protection from duplicate messages: 00412 $messageindex = intval($customdata['index']); 00413 00414 if($this->sets_info[$sessionid]['lastmessageindex'] >= $messageindex) { 00415 // We have already broadcasted that! 00416 // $this->trace('discarding message with stale index'); 00417 break; 00418 } 00419 else { 00420 // Update our info 00421 $this->sets_info[$sessionid]['lastmessageindex'] = $messageindex; 00422 } 00423 00424 $msg = New stdClass; 00425 $msg->chatid = $this->sets_info[$sessionid]['chatid']; 00426 $msg->userid = $this->sets_info[$sessionid]['userid']; 00427 $msg->groupid = $this->sets_info[$sessionid]['groupid']; 00428 $msg->system = 0; 00429 $msg->message = urldecode($customdata['message']); // have to undo the browser's encoding 00430 $msg->timestamp = time(); 00431 00432 if(empty($msg->message)) { 00433 // Someone just hit ENTER, send them on their way 00434 break; 00435 } 00436 00437 // A slight hack to prevent malformed SQL inserts 00438 $origmsg = $msg->message; 00439 $msg->message = $msg->message; 00440 00441 // Commit to DB 00442 $DB->insert_record('chat_messages', $msg, false); 00443 $DB->insert_record('chat_messages_current', $msg, false); 00444 00445 // Undo the hack 00446 $msg->message = $origmsg; 00447 00448 // OK, now push it out to all users 00449 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']); 00450 00451 // Update that user's lastmessageping 00452 $this->sets_info[$sessionid]['chatuser']->lastping = $msg->timestamp; 00453 $this->sets_info[$sessionid]['chatuser']->lastmessageping = $msg->timestamp; 00454 $this->user_lazy_update($sessionid); 00455 00456 // We did our work, but before slamming the door on the poor browser 00457 // show the courtesy of responding to the HTTP request. Otherwise, some 00458 // browsers decide to get vengeance by flooding us with repeat requests. 00459 00460 $header = "HTTP/1.1 200 OK\n"; 00461 $header .= "Connection: close\n"; 00462 $header .= "Date: ".date('r')."\n"; 00463 $header .= "Server: Moodle\n"; 00464 $header .= "Content-Type: text/html; charset=utf-8\n"; 00465 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 00466 $header .= "Cache-Control: no-cache, must-revalidate\n"; 00467 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 00468 $header .= "\n"; 00469 00470 // That's enough headers for one lousy dummy response 00471 $this->write_data($handle, $header); 00472 00473 // All done 00474 break; 00475 } 00476 00477 socket_shutdown($handle); 00478 socket_close($handle); 00479 } 00480 00481 function promote_final($sessionid, $customdata) { 00482 global $DB; 00483 00484 if(isset($this->conn_sets[$sessionid])) { 00485 $this->trace('Set cannot be finalized: Session '.$sessionid.' is already active'); 00486 return false; 00487 } 00488 00489 $chatuser = $DB->get_record('chat_users', array('sid'=>$sessionid)); 00490 if($chatuser === false) { 00491 $this->dismiss_half($sessionid); 00492 return false; 00493 } 00494 $chat = $DB->get_record('chat', array('id'=>$chatuser->chatid)); 00495 if($chat === false) { 00496 $this->dismiss_half($sessionid); 00497 return false; 00498 } 00499 $user = $DB->get_record('user', array('id'=>$chatuser->userid)); 00500 if($user === false) { 00501 $this->dismiss_half($sessionid); 00502 return false; 00503 } 00504 $course = $DB->get_record('course', array('id'=>$chat->course)); 00505 if($course === false) { 00506 $this->dismiss_half($sessionid); 00507 return false; 00508 } 00509 00510 global $CHAT_HTMLHEAD_JS, $CFG; 00511 00512 $this->conn_sets[$sessionid] = $this->conn_half[$sessionid]; 00513 00514 // This whole thing needs to be purged of redundant info, and the 00515 // code base to follow suit. But AFTER development is done. 00516 $this->sets_info[$sessionid] = array( 00517 'lastinfocommit' => 0, 00518 'lastmessageindex' => 0, 00519 'course' => $course, 00520 'courseid' => $course->id, 00521 'chatuser' => $chatuser, 00522 'chatid' => $chat->id, 00523 'user' => $user, 00524 'userid' => $user->id, 00525 'groupid' => $chatuser->groupid, 00526 'lang' => $chatuser->lang, 00527 'quirks' => $customdata['quirks'] 00528 ); 00529 00530 // If we know nothing about this chatroom, initialize it and add the user 00531 if(!isset($this->chatrooms[$chat->id]['users'])) { 00532 $this->chatrooms[$chat->id]['users'] = array($sessionid => $user->id); 00533 } 00534 else { 00535 // Otherwise just add the user 00536 $this->chatrooms[$chat->id]['users'][$sessionid] = $user->id; 00537 } 00538 00539 // $this->trace('QUIRKS value for this connection is '.$customdata['quirks']); 00540 00541 $this->dismiss_half($sessionid, false); 00542 $this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], $CHAT_HTMLHEAD_JS); 00543 $this->trace('Connection accepted: '.$this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL].', SID: '.$sessionid.' UID: '.$chatuser->userid.' GID: '.$chatuser->groupid, E_USER_WARNING); 00544 00545 // Finally, broadcast the "entered the chat" message 00546 00547 $msg = new stdClass; 00548 $msg->chatid = $chatuser->chatid; 00549 $msg->userid = $chatuser->userid; 00550 $msg->groupid = $chatuser->groupid; 00551 $msg->system = 1; 00552 $msg->message = 'enter'; 00553 $msg->timestamp = time(); 00554 00555 $DB->insert_record('chat_messages', $msg, false); 00556 $DB->insert_record('chat_messages_current', $msg, false); 00557 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']); 00558 00559 return true; 00560 } 00561 00562 function promote_ufo($handle, $type, $sessionid, $customdata) { 00563 if(empty($this->conn_ufo)) { 00564 return false; 00565 } 00566 foreach($this->conn_ufo as $id => $ufo) { 00567 if($ufo->handle == $handle) { 00568 // OK, got the id of the UFO, but what is it? 00569 00570 if($type & CHAT_SIDEKICK) { 00571 // Is the main connection ready? 00572 if(isset($this->conn_sets[$sessionid])) { 00573 // Yes, so dispatch this sidekick now and be done with it 00574 //$this->trace('Dispatching sidekick immediately'); 00575 $this->dispatch_sidekick($handle, $type, $sessionid, $customdata); 00576 $this->dismiss_ufo($handle, false); 00577 } 00578 else { 00579 // No, so put it in the waiting list 00580 $this->trace('sidekick waiting'); 00581 $this->conn_side[$sessionid][] = array('type' => $type, 'handle' => $handle, 'customdata' => $customdata); 00582 } 00583 return true; 00584 } 00585 00586 // If it's not a sidekick, at this point it can only be da man 00587 00588 if($type & CHAT_CONNECTION) { 00589 // This forces a new connection right now... 00590 $this->trace('Incoming connection from '.$ufo->ip.':'.$ufo->port); 00591 00592 // Do we have such a connection active? 00593 if(isset($this->conn_sets[$sessionid])) { 00594 // Yes, so regrettably we cannot promote you 00595 $this->trace('Connection rejected: session '.$sessionid.' is already final'); 00596 $this->dismiss_ufo($handle, true, 'Your SID was rejected.'); 00597 return false; 00598 } 00599 00600 // Join this with what we may have already 00601 $this->conn_half[$sessionid][$type] = $handle; 00602 00603 // Do the bookkeeping 00604 $this->promote_final($sessionid, $customdata); 00605 00606 // It's not an UFO anymore 00607 $this->dismiss_ufo($handle, false); 00608 00609 // Dispatch waiting sidekicks 00610 $this->process_sidekicks($sessionid); 00611 00612 return true; 00613 } 00614 } 00615 } 00616 return false; 00617 } 00618 00619 function dismiss_half($sessionid, $disconnect = true) { 00620 if(!isset($this->conn_half[$sessionid])) { 00621 return false; 00622 } 00623 if($disconnect) { 00624 foreach($this->conn_half[$sessionid] as $handle) { 00625 @socket_shutdown($handle); 00626 @socket_close($handle); 00627 } 00628 } 00629 unset($this->conn_half[$sessionid]); 00630 return true; 00631 } 00632 00633 function dismiss_set($sessionid) { 00634 if(!empty($this->conn_sets[$sessionid])) { 00635 foreach($this->conn_sets[$sessionid] as $handle) { 00636 // Since we want to dismiss this, don't generate any errors if it's dead already 00637 @socket_shutdown($handle); 00638 @socket_close($handle); 00639 } 00640 } 00641 $chatroom = $this->sets_info[$sessionid]['chatid']; 00642 $userid = $this->sets_info[$sessionid]['userid']; 00643 unset($this->conn_sets[$sessionid]); 00644 unset($this->sets_info[$sessionid]); 00645 unset($this->chatrooms[$chatroom]['users'][$sessionid]); 00646 $this->trace('Removed all traces of user with session '.$sessionid, E_USER_NOTICE); 00647 return true; 00648 } 00649 00650 00651 function dismiss_ufo($handle, $disconnect = true, $message = NULL) { 00652 if(empty($this->conn_ufo)) { 00653 return false; 00654 } 00655 foreach($this->conn_ufo as $id => $ufo) { 00656 if($ufo->handle == $handle) { 00657 unset($this->conn_ufo[$id]); 00658 if($disconnect) { 00659 if(!empty($message)) { 00660 $this->write_data($handle, $message."\n\n"); 00661 } 00662 socket_shutdown($handle); 00663 socket_close($handle); 00664 } 00665 return true; 00666 } 00667 } 00668 return false; 00669 } 00670 00671 function conn_accept() { 00672 $read_socket = array($this->listen_socket); 00673 $changed = socket_select($read_socket, $write = NULL, $except = NULL, 0, 0); 00674 00675 if(!$changed) { 00676 return false; 00677 } 00678 $handle = socket_accept($this->listen_socket); 00679 if(!$handle) { 00680 return false; 00681 } 00682 00683 $newconn = New ChatConnection($handle); 00684 $id = $this->new_ufo_id(); 00685 $this->conn_ufo[$id] = $newconn; 00686 00687 //$this->trace('UFO #'.$id.': connection from '.$newconn->ip.' on port '.$newconn->port.', '.$newconn->handle); 00688 } 00689 00690 function conn_activity_ufo (&$handles) { 00691 $monitor = array(); 00692 if(!empty($this->conn_ufo)) { 00693 foreach($this->conn_ufo as $ufoid => $ufo) { 00694 $monitor[$ufoid] = $ufo->handle; 00695 } 00696 } 00697 00698 if(empty($monitor)) { 00699 $handles = array(); 00700 return 0; 00701 } 00702 00703 $retval = socket_select($monitor, $a = NULL, $b = NULL, NULL); 00704 $handles = $monitor; 00705 00706 return $retval; 00707 } 00708 00709 function message_broadcast($message, $sender) { 00710 global $PAGE; 00711 00712 if(empty($this->conn_sets)) { 00713 return true; 00714 } 00715 00716 $now = time(); 00717 00718 // First of all, mark this chatroom as having had activity now 00719 $this->chatrooms[$message->chatid]['lastactivity'] = $now; 00720 00721 foreach($this->sets_info as $sessionid => $info) { 00722 // We need to get handles from users that are in the same chatroom, same group 00723 if($info['chatid'] == $message->chatid && 00724 ($info['groupid'] == $message->groupid || $message->groupid == 0)) 00725 { 00726 00727 // Simply give them the message 00728 $PAGE->set_course($info['course']); 00729 $output = chat_format_message_manually($message, $info['courseid'], $sender, $info['user']); 00730 $this->trace('Delivering message "'.$output->text.'" to '.$this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL]); 00731 00732 if($output->beep) { 00733 $this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], '<embed src="'.$this->_beepsoundsrc.'" autostart="true" hidden="true" />'); 00734 } 00735 00736 if($info['quirks'] & QUIRK_CHUNK_UPDATE) { 00737 $output->html .= $GLOBALS['CHAT_DUMMY_DATA']; 00738 $output->html .= $GLOBALS['CHAT_DUMMY_DATA']; 00739 $output->html .= $GLOBALS['CHAT_DUMMY_DATA']; 00740 } 00741 00742 if(!$this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], $output->html)) { 00743 $this->disconnect_session($sessionid); 00744 } 00745 //$this->trace('Sent to UID '.$this->sets_info[$sessionid]['userid'].': '.$message->text_); 00746 } 00747 } 00748 } 00749 00750 function disconnect_session($sessionid) { 00751 global $DB; 00752 00753 $info = $this->sets_info[$sessionid]; 00754 00755 $DB->delete_records('chat_users', array('sid'=>$sessionid)); 00756 $msg = New stdClass; 00757 $msg->chatid = $info['chatid']; 00758 $msg->userid = $info['userid']; 00759 $msg->groupid = $info['groupid']; 00760 $msg->system = 1; 00761 $msg->message = 'exit'; 00762 $msg->timestamp = time(); 00763 00764 $this->trace('User has disconnected, destroying uid '.$info['userid'].' with SID '.$sessionid, E_USER_WARNING); 00765 $DB->insert_record('chat_messages', $msg, false); 00766 $DB->insert_record('chat_messages_current', $msg, false); 00767 00768 // *************************** IMPORTANT 00769 // 00770 // Kill him BEFORE broadcasting, otherwise we 'll get infinite recursion! 00771 // 00772 // ********************************************************************** 00773 $latesender = $info['user']; 00774 $this->dismiss_set($sessionid); 00775 $this->message_broadcast($msg, $latesender); 00776 } 00777 00778 function fatal($message) { 00779 $message .= "\n"; 00780 if($this->_logfile) { 00781 $this->trace($message, E_USER_ERROR); 00782 } 00783 echo "FATAL ERROR:: $message\n"; 00784 die(); 00785 } 00786 00787 function init_sockets() { 00788 global $CFG; 00789 00790 $this->trace('Setting up sockets'); 00791 00792 if(false === ($this->listen_socket = socket_create(AF_INET, SOCK_STREAM, 0))) { 00793 // Failed to create socket 00794 $lasterr = socket_last_error(); 00795 $this->fatal('socket_create() failed: '. socket_strerror($lasterr).' ['.$lasterr.']'); 00796 } 00797 00798 //socket_close($DAEMON->listen_socket); 00799 //die(); 00800 00801 if(!socket_bind($this->listen_socket, $CFG->chat_serverip, $CFG->chat_serverport)) { 00802 // Failed to bind socket 00803 $lasterr = socket_last_error(); 00804 $this->fatal('socket_bind() failed: '. socket_strerror($lasterr).' ['.$lasterr.']'); 00805 } 00806 00807 if(!socket_listen($this->listen_socket, $CFG->chat_servermax)) { 00808 // Failed to get socket to listen 00809 $lasterr = socket_last_error(); 00810 $this->fatal('socket_listen() failed: '. socket_strerror($lasterr).' ['.$lasterr.']'); 00811 } 00812 00813 // Socket has been initialized and is ready 00814 $this->trace('Socket opened on port '.$CFG->chat_serverport); 00815 00816 // [pj]: I really must have a good read on sockets. What exactly does this do? 00817 // http://www.unixguide.net/network/socketfaq/4.5.shtml is still not enlightening enough for me. 00818 socket_setopt($this->listen_socket, SOL_SOCKET, SO_REUSEADDR, 1); 00819 socket_set_nonblock($this->listen_socket); 00820 } 00821 00822 function cli_switch($switch, $param = NULL) { 00823 switch($switch) { //LOL 00824 case 'reset': 00825 // Reset sockets 00826 $this->_resetsocket = true; 00827 return false; 00828 case 'start': 00829 // Start the daemon 00830 $this->_readytogo = true; 00831 return false; 00832 break; 00833 case 'v': 00834 // Verbose mode 00835 $this->_trace_level = E_ALL; 00836 return false; 00837 break; 00838 case 'l': 00839 // Use logfile 00840 if(!empty($param)) { 00841 $this->_logfile_name = $param; 00842 } 00843 $this->_logfile = @fopen($this->_logfile_name, 'a+'); 00844 if($this->_logfile == false) { 00845 $this->fatal('Failed to open '.$this->_logfile_name.' for writing'); 00846 } 00847 return false; 00848 default: 00849 // Unrecognized 00850 $this->fatal('Unrecognized command line switch: '.$switch); 00851 break; 00852 } 00853 return false; 00854 } 00855 00856 } 00857 00858 $DAEMON = New ChatDaemon; 00859 set_error_handler(array($DAEMON, 'error_handler')); 00860 00862 00863 unset($argv[0]); 00864 $commandline = implode(' ', $argv); 00865 if(strpos($commandline, '-') === false) { 00866 if(!empty($commandline)) { 00867 // We cannot have received any meaningful parameters 00868 $DAEMON->fatal('Garbage in command line'); 00869 } 00870 } 00871 else { 00872 // Parse command line 00873 $switches = preg_split('/(-{1,2}[a-zA-Z]+) */', $commandline, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); 00874 00875 // Taking advantage of the fact that $switches is indexed with incrementing numeric keys 00876 // We will be using that to pass additional information to those switches who need it 00877 $numswitches = count($switches); 00878 00879 // Fancy way to give a "hyphen" boolean flag to each "switch" 00880 $switches = array_map(create_function('$x', 'return array("str" => $x, "hyphen" => (substr($x, 0, 1) == "-"));'), $switches); 00881 00882 for($i = 0; $i < $numswitches; ++$i) { 00883 00884 $switch = $switches[$i]['str']; 00885 $params = ($i == $numswitches - 1 ? NULL : 00886 ($switches[$i + 1]['hyphen'] ? NULL : trim($switches[$i + 1]['str'])) 00887 ); 00888 00889 if(substr($switch, 0, 2) == '--') { 00890 // Double-hyphen switch 00891 $DAEMON->cli_switch(strtolower(substr($switch, 2)), $params); 00892 } 00893 else if(substr($switch, 0, 1) == '-') { 00894 // Single-hyphen switch(es), may be more than one run together 00895 $switch = substr($switch, 1); // Get rid of the - 00896 $len = strlen($switch); 00897 for($j = 0; $j < $len; ++$j) { 00898 $DAEMON->cli_switch(strtolower(substr($switch, $j, 1)), $params); 00899 } 00900 } 00901 } 00902 } 00903 00904 if(!$DAEMON->query_start()) { 00905 // For some reason we didn't start, so print out some info 00906 echo 'Starts the Moodle chat socket server on port '.$CFG->chat_serverport; 00907 echo "\n\n"; 00908 echo "Usage: chatd.php [parameters]\n\n"; 00909 echo "Parameters:\n"; 00910 echo " --start Starts the daemon\n"; 00911 echo " -v Verbose mode (prints trivial information messages)\n"; 00912 echo " -l [logfile] Log all messages to logfile (if not specified, chatd.log)\n"; 00913 echo "Example:\n"; 00914 echo " chatd.php --start -l\n\n"; 00915 die(); 00916 } 00917 00918 if (!function_exists('socket_setopt')) { 00919 echo "Error: Function socket_setopt() does not exist.\n"; 00920 echo "Possibly PHP has not been compiled with --enable-sockets.\n\n"; 00921 die(); 00922 } 00923 00924 $DAEMON->init_sockets(); 00925 00926 /* 00927 declare(ticks=1); 00928 00929 $pid = pcntl_fork(); 00930 if ($pid == -1) { 00931 die("could not fork"); 00932 } else if ($pid) { 00933 exit(); // we are the parent 00934 } else { 00935 // we are the child 00936 } 00937 00938 // detatch from the controlling terminal 00939 if (!posix_setsid()) { 00940 die("could not detach from terminal"); 00941 } 00942 00943 // setup signal handlers 00944 pcntl_signal(SIGTERM, "sig_handler"); 00945 pcntl_signal(SIGHUP, "sig_handler"); 00946 00947 if($DAEMON->_pcntl_exists && false) { 00948 $DAEMON->trace('Unholy spirit possession: daemonizing'); 00949 $DAEMON->pid = pcntl_fork(); 00950 if($pid == -1) { 00951 $DAEMON->trace('Process fork failed, terminating'); 00952 die(); 00953 } 00954 else if($pid) { 00955 // We are the parent 00956 $DAEMON->trace('Successfully forked the daemon with PID '.$pid); 00957 die(); 00958 } 00959 else { 00960 // We are the daemon! :P 00961 } 00962 00963 // FROM NOW ON, IT'S THE DAEMON THAT'S RUNNING! 00964 00965 // Detach from controlling terminal 00966 if(!posix_setsid()) { 00967 $DAEMON->trace('Could not detach daemon process from terminal!'); 00968 } 00969 } 00970 else { 00971 // Cannot go demonic 00972 $DAEMON->trace('Unholy spirit possession failed: PHP is not compiled with --enable-pcntl'); 00973 } 00974 */ 00975 00976 $DAEMON->trace('Started Moodle chatd on port '.$CFG->chat_serverport.', listening socket '.$DAEMON->listen_socket, E_USER_WARNING); 00977 00979 $DB->delete_records('chat_users', array('version'=>'sockets')); 00980 00981 while(true) { 00982 $active = array(); 00983 00984 // First of all, let's see if any of our UFOs has identified itself 00985 if($DAEMON->conn_activity_ufo($active)) { 00986 foreach($active as $handle) { 00987 $read_socket = array($handle); 00988 $changed = socket_select($read_socket, $write = NULL, $except = NULL, 0, 0); 00989 00990 if($changed > 0) { 00991 // Let's see what it has to say 00992 00993 $data = socket_read($handle, 2048); // should be more than 512 to prevent empty pages and repeated messages!! 00994 if(empty($data)) { 00995 continue; 00996 } 00997 00998 if (strlen($data) == 2048) { // socket_read has more data, ignore all data 00999 $DAEMON->trace('UFO with '.$handle.': Data too long; connection closed', E_USER_WARNING); 01000 $DAEMON->dismiss_ufo($handle, true, 'Data too long; connection closed'); 01001 continue; 01002 } 01003 01004 if(!preg_match('/win=(chat|users|message|beep).*&chat_sid=([a-zA-Z0-9]*) HTTP/', $data, $info)) { 01005 // Malformed data 01006 $DAEMON->trace('UFO with '.$handle.': Request with malformed data; connection closed', E_USER_WARNING); 01007 $DAEMON->dismiss_ufo($handle, true, 'Request with malformed data; connection closed'); 01008 continue; 01009 } 01010 01011 $type = $info[1]; 01012 $sessionid = $info[2]; 01013 01014 $customdata = array(); 01015 01016 switch($type) { 01017 case 'chat': 01018 $type = CHAT_CONNECTION_CHANNEL; 01019 $customdata['quirks'] = 0; 01020 if(strpos($data, 'Safari')) { 01021 $DAEMON->trace('Safari identified...', E_USER_WARNING); 01022 $customdata['quirks'] += QUIRK_CHUNK_UPDATE; 01023 } 01024 break; 01025 case 'users': 01026 $type = CHAT_SIDEKICK_USERS; 01027 break; 01028 case 'beep': 01029 $type = CHAT_SIDEKICK_BEEP; 01030 if(!preg_match('/beep=([^&]*)[& ]/', $data, $info)) { 01031 $DAEMON->trace('Beep sidekick did not contain a valid userid', E_USER_WARNING); 01032 $DAEMON->dismiss_ufo($handle, true, 'Request with malformed data; connection closed'); 01033 continue; 01034 } 01035 else { 01036 $customdata = array('beep' => intval($info[1])); 01037 } 01038 break; 01039 case 'message': 01040 $type = CHAT_SIDEKICK_MESSAGE; 01041 if(!preg_match('/chat_message=([^&]*)[& ]chat_msgidnr=([^&]*)[& ]/', $data, $info)) { 01042 $DAEMON->trace('Message sidekick did not contain a valid message', E_USER_WARNING); 01043 $DAEMON->dismiss_ufo($handle, true, 'Request with malformed data; connection closed'); 01044 continue; 01045 } 01046 else { 01047 $customdata = array('message' => $info[1], 'index' => $info[2]); 01048 } 01049 break; 01050 default: 01051 $DAEMON->trace('UFO with '.$handle.': Request with unknown type; connection closed', E_USER_WARNING); 01052 $DAEMON->dismiss_ufo($handle, true, 'Request with unknown type; connection closed'); 01053 continue; 01054 break; 01055 } 01056 01057 // OK, now we know it's something good... promote it and pass it all the data it needs 01058 $DAEMON->promote_ufo($handle, $type, $sessionid, $customdata); 01059 continue; 01060 } 01061 } 01062 } 01063 01064 $now = time(); 01065 01066 // Clean up chatrooms with no activity as required 01067 if($now - $DAEMON->_last_idle_poll >= $DAEMON->_freq_poll_idle_chat) { 01068 $DAEMON->poll_idle_chats($now); 01069 } 01070 01071 // Finally, accept new connections 01072 $DAEMON->conn_accept(); 01073 01074 usleep($DAEMON->_time_rest_socket); 01075 } 01076 01077 @socket_shutdown($DAEMON->listen_socket, 0); 01078 die("\n\n-- terminated --\n"); 01079 01080