[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Chat daemon 19 * 20 * @package mod_chat 21 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 define('CLI_SCRIPT', true); 26 27 require(dirname(dirname(dirname(__FILE__))).'/config.php'); 28 require_once($CFG->dirroot . '/mod/chat/lib.php'); 29 30 // Browser quirks. 31 define('QUIRK_CHUNK_UPDATE', 0x0001); 32 33 // Connection telltale. 34 define('CHAT_CONNECTION', 0x10); 35 // Connections: Incrementing sequence, 0x10 to 0x1f. 36 define('CHAT_CONNECTION_CHANNEL', 0x11); 37 38 // Sidekick telltale. 39 define('CHAT_SIDEKICK', 0x20); 40 // Sidekicks: Incrementing sequence, 0x21 to 0x2f. 41 define('CHAT_SIDEKICK_USERS', 0x21); 42 define('CHAT_SIDEKICK_MESSAGE', 0x22); 43 define('CHAT_SIDEKICK_BEEP', 0x23); 44 45 $phpversion = phpversion(); 46 echo 'Moodle chat daemon v1.0 on PHP '.$phpversion."\n\n"; 47 48 // Set up all the variables we need. 49 50 // The $CFG variables are now defined in database by chat/lib.php. 51 52 $_SERVER['PHP_SELF'] = 'dummy'; 53 $_SERVER['SERVER_NAME'] = 'dummy'; 54 $_SERVER['HTTP_USER_AGENT'] = 'dummy'; 55 56 $_SERVER['SERVER_NAME'] = $CFG->chat_serverhost; 57 $_SERVER['PHP_SELF'] = "http://$CFG->chat_serverhost:$CFG->chat_serverport/mod/chat/chatd.php"; 58 59 core_php_time_limit::raise(0); 60 error_reporting(E_ALL); 61 62 function chat_empty_connection() { 63 return array('sid' => null, 'handle' => null, 'ip' => null, 'port' => null, 'groupid' => null); 64 } 65 66 class ChatConnection { 67 // Chat-related info. 68 public $sid = null; 69 public $type = null; 70 71 // PHP-level info. 72 public $handle = null; 73 74 // TCP/IP. 75 public $ip = null; 76 public $port = null; 77 78 public function __construct($resource) { 79 $this->handle = $resource; 80 @socket_getpeername($this->handle, $this->ip, $this->port); 81 } 82 } 83 84 class ChatDaemon { 85 public $_resetsocket = false; 86 public $_readytogo = false; 87 public $_logfile = false; 88 public $_trace_to_console = true; 89 public $_trace_to_stdout = true; 90 public $_logfile_name = 'chatd.log'; 91 public $_last_idle_poll = 0; 92 93 public $connectionsunidentified = array(); // Connections not identified yet. 94 public $connectionsside = array(); // Sessions with sidekicks waiting for the main connection to be processed. 95 public $connectionshalf = array(); // Sessions that have valid connections but not all of them. 96 public $connectionssets = array(); // Sessions with complete connection sets. 97 public $setsinfo = array(); // Keyed by sessionid exactly like conn_sets, one of these for each of those. 98 public $chatrooms = array(); // Keyed by chatid, holding arrays of data. 99 100 // IMPORTANT: $connectionssets, $setsinfo and $chatrooms must remain synchronized! 101 // Pay extra attention when you write code that affects any of them! 102 103 public function __construct() { 104 $this->_trace_level = E_ALL ^ E_USER_NOTICE; 105 $this->_pcntl_exists = function_exists('pcntl_fork'); 106 $this->_time_rest_socket = 20; 107 $this->_beepsoundsrc = $GLOBALS['CFG']->wwwroot.'/mod/chat/beep.wav'; 108 $this->_freq_update_records = 20; 109 $this->_freq_poll_idle_chat = $GLOBALS['CFG']->chat_old_ping; 110 $this->_stdout = fopen('php://stdout', 'w'); 111 if ($this->_stdout) { 112 // Avoid double traces for everything. 113 $this->_trace_to_console = false; 114 } 115 } 116 117 public function error_handler ($errno, $errmsg, $filename, $linenum, $vars) { 118 // Checks if an error needs to be suppressed due to @. 119 if (error_reporting() != 0) { 120 $this->trace($errmsg.' on line '.$linenum, $errno); 121 } 122 return true; 123 } 124 125 public function poll_idle_chats($now) { 126 $this->trace('Polling chats to detect disconnected users'); 127 if (!empty($this->chatrooms)) { 128 foreach ($this->chatrooms as $chatid => $chatroom) { 129 if (!empty($chatroom['users'])) { 130 foreach ($chatroom['users'] as $sessionid => $userid) { 131 // We will be polling each user as required. 132 $this->trace('...shall we poll '.$sessionid.'?'); 133 if ($this->sets_info[$sessionid]['chatuser']->lastmessageping < $this->_last_idle_poll) { 134 $this->trace('YES!'); 135 // This user hasn't been polled since his last message. 136 $result = $this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], '<!-- poll -->'); 137 if ($result === false) { 138 // User appears to have disconnected. 139 $this->disconnect_session($sessionid); 140 } 141 } 142 } 143 } 144 } 145 } 146 $this->_last_idle_poll = $now; 147 } 148 149 public function query_start() { 150 return $this->_readytogo; 151 } 152 153 public function trace($message, $level = E_USER_NOTICE) { 154 $severity = ''; 155 156 switch($level) { 157 case E_USER_WARNING: 158 $severity = '*IMPORTANT* '; 159 break; 160 case E_USER_ERROR: 161 $severity = ' *CRITICAL* '; 162 break; 163 case E_NOTICE: 164 case E_WARNING: 165 $severity = ' *CRITICAL* [php] '; 166 break; 167 } 168 169 $date = date('[Y-m-d H:i:s] '); 170 $message = $date.$severity.$message."\n"; 171 172 if ($this->_trace_level & $level) { 173 // It is accepted for output. 174 175 // Error-class traces go to STDERR too. 176 if ($level & E_USER_ERROR) { 177 fwrite(STDERR, $message); 178 } 179 180 // Emit the message to wherever we should. 181 if ($this->_trace_to_stdout) { 182 fwrite($this->_stdout, $message); 183 fflush($this->_stdout); 184 } 185 if ($this->_trace_to_console) { 186 echo $message; 187 flush(); 188 } 189 if ($this->_logfile) { 190 fwrite($this->_logfile, $message); 191 fflush($this->_logfile); 192 } 193 } 194 } 195 196 public function write_data($connection, $text) { 197 $written = @socket_write($connection, $text, strlen($text)); 198 if ($written === false) { 199 return false; 200 } 201 return true; 202 } 203 204 public function user_lazy_update($sessionid) { 205 global $DB; 206 207 if (empty($this->sets_info[$sessionid])) { 208 $this->trace('user_lazy_update() called for an invalid SID: '.$sessionid, E_USER_WARNING); 209 return false; 210 } 211 212 $now = time(); 213 214 // We 'll be cheating a little, and NOT updating the record data as 215 // often as we can, so that we save on DB queries (imagine MANY users). 216 if ($now - $this->sets_info[$sessionid]['lastinfocommit'] > $this->_freq_update_records) { 217 // Commit to permanent storage. 218 $this->sets_info[$sessionid]['lastinfocommit'] = $now; 219 $DB->update_record('chat_users', $this->sets_info[$sessionid]['chatuser']); 220 } 221 return true; 222 } 223 224 public function get_user_window($sessionid) { 225 global $CFG, $OUTPUT; 226 227 static $str; 228 229 $info = &$this->sets_info[$sessionid]; 230 231 $timenow = time(); 232 233 if (empty($str)) { 234 $str->idle = get_string("idle", "chat"); 235 $str->beep = get_string("beep", "chat"); 236 $str->day = get_string("day"); 237 $str->days = get_string("days"); 238 $str->hour = get_string("hour"); 239 $str->hours = get_string("hours"); 240 $str->min = get_string("min"); 241 $str->mins = get_string("mins"); 242 $str->sec = get_string("sec"); 243 $str->secs = get_string("secs"); 244 $str->years = get_string('years'); 245 } 246 247 ob_start(); 248 $refreshinval = $CFG->chat_refresh_userlist * 1000; 249 echo <<<EOD 250 <html><head> 251 <meta http-equiv="refresh" content="$refreshinval"> 252 <style type="text/css"> img{border:0} </style> 253 <script type="text/javascript"> 254 //<![CDATA[ 255 function openpopup(url,name,options,fullscreen) { 256 fullurl = "$CFG->wwwroot" + url; 257 windowobj = window.open(fullurl,name,options); 258 if (fullscreen) { 259 windowobj.moveTo(0,0); 260 windowobj.resizeTo(screen.availWidth,screen.availHeight); 261 } 262 windowobj.focus(); 263 return false; 264 } 265 //]]> 266 </script></head><body><table><tbody> 267 EOD; 268 269 // Get the users from that chatroom. 270 $users = $this->chatrooms[$info['chatid']]['users']; 271 272 foreach ($users as $usersessionid => $userid) { 273 // Fetch each user's sessionid and then the rest of his data from $this->sets_info. 274 $userinfo = $this->sets_info[$usersessionid]; 275 276 $lastping = $timenow - $userinfo['chatuser']->lastmessageping; 277 278 echo '<tr><td width="35">'; 279 280 $link = '/user/view.php?id='.$userinfo['user']->id.'&course='.$userinfo['courseid']; 281 $anchortagcontents = $OUTPUT->user_picture($userinfo['user'], array('courseid' => $userinfo['courseid'])); 282 283 $action = new popup_action('click', $link, 'user'.$userinfo['chatuser']->id); 284 $anchortag = $OUTPUT->action_link($link, $anchortagcontents, $action); 285 286 echo $anchortag; 287 echo "</td><td valign=\"center\">"; 288 echo "<p><font size=\"1\">"; 289 echo fullname($userinfo['user'])."<br />"; 290 echo "<font color=\"#888888\">$str->idle: ".format_time($lastping, $str)."</font> "; 291 echo '<a target="empty" href="http://'.$CFG->chat_serverhost.':'.$CFG->chat_serverport. 292 '/?win=beep&beep='.$userinfo['user']->id. 293 '&chat_sid='.$sessionid.'">'.$str->beep."</a>\n"; 294 echo "</font></p>"; 295 echo "<td></tr>"; 296 } 297 298 echo '</tbody></table>'; 299 echo "</body>\n</html>\n"; 300 301 return ob_get_clean(); 302 303 } 304 305 public function new_ufo_id() { 306 static $id = 0; 307 if ($id++ === 0x1000000) { // Cycling very very slowly to prevent overflow. 308 $id = 0; 309 } 310 return $id; 311 } 312 313 public function process_sidekicks($sessionid) { 314 if (empty($this->conn_side[$sessionid])) { 315 return true; 316 } 317 foreach ($this->conn_side[$sessionid] as $sideid => $sidekick) { 318 // TODO: is this late-dispatch working correctly? 319 $this->dispatch_sidekick($sidekick['handle'], $sidekick['type'], $sessionid, $sidekick['customdata']); 320 unset($this->conn_side[$sessionid][$sideid]); 321 } 322 return true; 323 } 324 325 public function dispatch_sidekick($handle, $type, $sessionid, $customdata) { 326 global $CFG, $DB; 327 328 switch($type) { 329 case CHAT_SIDEKICK_BEEP: 330 331 // Incoming beep. 332 $msg = new stdClass; 333 $msg->chatid = $this->sets_info[$sessionid]['chatid']; 334 $msg->userid = $this->sets_info[$sessionid]['userid']; 335 $msg->groupid = $this->sets_info[$sessionid]['groupid']; 336 $msg->system = 0; 337 $msg->message = 'beep '.$customdata['beep']; 338 $msg->timestamp = time(); 339 340 // Commit to DB. 341 chat_send_chatmessage($this->sets_info[$sessionid]['chatuser'], $msg->message, false, 342 $this->sets_info[$sessionid]['cm']); 343 344 // OK, now push it out to all users. 345 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']); 346 347 // Update that user's lastmessageping. 348 $this->sets_info[$sessionid]['chatuser']->lastping = $msg->timestamp; 349 $this->sets_info[$sessionid]['chatuser']->lastmessageping = $msg->timestamp; 350 $this->user_lazy_update($sessionid); 351 352 // We did our work, but before slamming the door on the poor browser 353 // show the courtesy of responding to the HTTP request. Otherwise, some 354 // browsers decide to get vengeance by flooding us with repeat requests. 355 356 $header = "HTTP/1.1 200 OK\n"; 357 $header .= "Connection: close\n"; 358 $header .= "Date: ".date('r')."\n"; 359 $header .= "Server: Moodle\n"; 360 $header .= "Content-Type: text/html; charset=utf-8\n"; 361 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 362 $header .= "Cache-Control: no-cache, must-revalidate\n"; 363 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 364 $header .= "\n"; 365 366 // That's enough headers for one lousy dummy response. 367 $this->write_data($handle, $header); 368 // All done. 369 break; 370 371 case CHAT_SIDEKICK_USERS: 372 // A request to paint a user window. 373 374 $content = $this->get_user_window($sessionid); 375 376 $header = "HTTP/1.1 200 OK\n"; 377 $header .= "Connection: close\n"; 378 $header .= "Date: ".date('r')."\n"; 379 $header .= "Server: Moodle\n"; 380 $header .= "Content-Type: text/html; charset=utf-8\n"; 381 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 382 $header .= "Cache-Control: no-cache, must-revalidate\n"; 383 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 384 $header .= "Content-Length: ".strlen($content)."\n"; 385 386 // The refresh value is 2 seconds higher than the configuration variable. 387 // This is because we are doing JS refreshes all the time. 388 // However, if the JS doesn't work for some reason, we still want to refresh once in a while. 389 $header .= "Refresh: ".(intval($CFG->chat_refresh_userlist) + 2). 390 "; url=http://$CFG->chat_serverhost:$CFG->chat_serverport/?win=users&". 391 "chat_sid=".$sessionid."\n"; 392 $header .= "\n"; 393 394 // That's enough headers for one lousy dummy response. 395 $this->trace('writing users http response to handle '.$handle); 396 $this->write_data($handle, $header . $content); 397 398 // Update that user's lastping. 399 $this->sets_info[$sessionid]['chatuser']->lastping = time(); 400 $this->user_lazy_update($sessionid); 401 402 break; 403 404 case CHAT_SIDEKICK_MESSAGE: 405 // Incoming message. 406 407 // Browser stupidity protection from duplicate messages. 408 $messageindex = intval($customdata['index']); 409 410 if ($this->sets_info[$sessionid]['lastmessageindex'] >= $messageindex) { 411 // We have already broadcasted that! 412 break; 413 } else { 414 // Update our info. 415 $this->sets_info[$sessionid]['lastmessageindex'] = $messageindex; 416 } 417 418 $msg = new stdClass; 419 $msg->chatid = $this->sets_info[$sessionid]['chatid']; 420 $msg->userid = $this->sets_info[$sessionid]['userid']; 421 $msg->groupid = $this->sets_info[$sessionid]['groupid']; 422 $msg->system = 0; 423 $msg->message = urldecode($customdata['message']); // Have to undo the browser's encoding. 424 $msg->timestamp = time(); 425 426 if (empty($msg->message)) { 427 // Someone just hit ENTER, send them on their way. 428 break; 429 } 430 431 // A slight hack to prevent malformed SQL inserts. 432 $origmsg = $msg->message; 433 $msg->message = $msg->message; 434 435 // Commit to DB. 436 chat_send_chatmessage($this->sets_info[$sessionid]['chatuser'], $msg->message, false, 437 $this->sets_info[$sessionid]['cm']); 438 439 // Undo the hack. 440 $msg->message = $origmsg; 441 442 // OK, now push it out to all users. 443 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']); 444 445 // Update that user's lastmessageping. 446 $this->sets_info[$sessionid]['chatuser']->lastping = $msg->timestamp; 447 $this->sets_info[$sessionid]['chatuser']->lastmessageping = $msg->timestamp; 448 $this->user_lazy_update($sessionid); 449 450 // We did our work, but before slamming the door on the poor browser 451 // show the courtesy of responding to the HTTP request. Otherwise, some 452 // browsers decide to get vengeance by flooding us with repeat requests. 453 454 $header = "HTTP/1.1 200 OK\n"; 455 $header .= "Connection: close\n"; 456 $header .= "Date: ".date('r')."\n"; 457 $header .= "Server: Moodle\n"; 458 $header .= "Content-Type: text/html; charset=utf-8\n"; 459 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 460 $header .= "Cache-Control: no-cache, must-revalidate\n"; 461 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 462 $header .= "\n"; 463 464 // That's enough headers for one lousy dummy response. 465 $this->write_data($handle, $header); 466 467 // All done. 468 break; 469 } 470 471 socket_shutdown($handle); 472 socket_close($handle); 473 } 474 475 public function promote_final($sessionid, $customdata) { 476 global $DB; 477 478 if (isset($this->conn_sets[$sessionid])) { 479 $this->trace('Set cannot be finalized: Session '.$sessionid.' is already active'); 480 return false; 481 } 482 483 $chatuser = $DB->get_record('chat_users', array('sid' => $sessionid)); 484 if ($chatuser === false) { 485 $this->dismiss_half($sessionid); 486 return false; 487 } 488 $chat = $DB->get_record('chat', array('id' => $chatuser->chatid)); 489 if ($chat === false) { 490 $this->dismiss_half($sessionid); 491 return false; 492 } 493 $user = $DB->get_record('user', array('id' => $chatuser->userid)); 494 if ($user === false) { 495 $this->dismiss_half($sessionid); 496 return false; 497 } 498 $course = $DB->get_record('course', array('id' => $chat->course)); 499 if ($course === false) { 500 $this->dismiss_half($sessionid); 501 return false; 502 } 503 if (!($cm = get_coursemodule_from_instance('chat', $chat->id, $course->id))) { 504 $this->dismiss_half($sessionid); 505 return false; 506 } 507 508 global $CHAT_HTMLHEAD_JS; 509 510 $this->conn_sets[$sessionid] = $this->conn_half[$sessionid]; 511 512 // This whole thing needs to be purged of redundant info, and the 513 // code base to follow suit. But AFTER development is done. 514 $this->sets_info[$sessionid] = array( 515 'lastinfocommit' => 0, 516 'lastmessageindex' => 0, 517 'course' => $course, 518 'courseid' => $course->id, 519 'chatuser' => $chatuser, 520 'chatid' => $chat->id, 521 'cm' => $cm, 522 'user' => $user, 523 'userid' => $user->id, 524 'groupid' => $chatuser->groupid, 525 'lang' => $chatuser->lang, 526 'quirks' => $customdata['quirks'] 527 ); 528 529 // If we know nothing about this chatroom, initialize it and add the user. 530 if (!isset($this->chatrooms[$chat->id]['users'])) { 531 $this->chatrooms[$chat->id]['users'] = array($sessionid => $user->id); 532 } else { 533 // Otherwise just add the user. 534 $this->chatrooms[$chat->id]['users'][$sessionid] = $user->id; 535 } 536 537 $header = "HTTP/1.1 200 OK\n"; 538 $header .= "Connection: close\n"; 539 $header .= "Date: ".date('r')."\n"; 540 $header .= "Server: Moodle\n"; 541 $header .= "Content-Type: text/html; charset=utf-8\n"; 542 $header .= "Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT\n"; 543 $header .= "Cache-Control: no-cache, must-revalidate\n"; 544 $header .= "Expires: Wed, 4 Oct 1978 09:32:45 GMT\n"; 545 $header .= "\n"; 546 547 $this->dismiss_half($sessionid, false); 548 $this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], $header . $CHAT_HTMLHEAD_JS); 549 $this->trace('Connection accepted: ' 550 .$this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL] 551 .', SID: ' 552 .$sessionid 553 .' UID: ' 554 .$chatuser->userid 555 .' GID: ' 556 .$chatuser->groupid, E_USER_WARNING); 557 558 // Finally, broadcast the "entered the chat" message. 559 560 $msg = new stdClass; 561 $msg->chatid = $chatuser->chatid; 562 $msg->userid = $chatuser->userid; 563 $msg->groupid = $chatuser->groupid; 564 $msg->system = 1; 565 $msg->message = 'enter'; 566 $msg->timestamp = time(); 567 568 chat_send_chatmessage($chatuser, $msg->message, true); 569 $this->message_broadcast($msg, $this->sets_info[$sessionid]['user']); 570 571 return true; 572 } 573 574 public function promote_ufo($handle, $type, $sessionid, $customdata) { 575 if (empty($this->conn_ufo)) { 576 return false; 577 } 578 foreach ($this->conn_ufo as $id => $ufo) { 579 if ($ufo->handle == $handle) { 580 // OK, got the id of the UFO, but what is it? 581 582 if ($type & CHAT_SIDEKICK) { 583 // Is the main connection ready? 584 if (isset($this->conn_sets[$sessionid])) { 585 // Yes, so dispatch this sidekick now and be done with it. 586 $this->dispatch_sidekick($handle, $type, $sessionid, $customdata); 587 $this->dismiss_ufo($handle, false); 588 } else { 589 // No, so put it in the waiting list. 590 $this->trace('sidekick waiting'); 591 $this->conn_side[$sessionid][] = array('type' => $type, 'handle' => $handle, 'customdata' => $customdata); 592 } 593 return true; 594 } 595 596 // If it's not a sidekick, at this point it can only be da man. 597 598 if ($type & CHAT_CONNECTION) { 599 // This forces a new connection right now. 600 $this->trace('Incoming connection from '.$ufo->ip.':'.$ufo->port); 601 602 // Do we have such a connection active? 603 if (isset($this->conn_sets[$sessionid])) { 604 // Yes, so regrettably we cannot promote you. 605 $this->trace('Connection rejected: session '.$sessionid.' is already final'); 606 $this->dismiss_ufo($handle, true, 'Your SID was rejected.'); 607 return false; 608 } 609 610 // Join this with what we may have already. 611 $this->conn_half[$sessionid][$type] = $handle; 612 613 // Do the bookkeeping. 614 $this->promote_final($sessionid, $customdata); 615 616 // It's not a UFO anymore. 617 $this->dismiss_ufo($handle, false); 618 619 // Dispatch waiting sidekicks. 620 $this->process_sidekicks($sessionid); 621 622 return true; 623 } 624 } 625 } 626 return false; 627 } 628 629 public function dismiss_half($sessionid, $disconnect = true) { 630 if (!isset($this->conn_half[$sessionid])) { 631 return false; 632 } 633 if ($disconnect) { 634 foreach ($this->conn_half[$sessionid] as $handle) { 635 @socket_shutdown($handle); 636 @socket_close($handle); 637 } 638 } 639 unset($this->conn_half[$sessionid]); 640 return true; 641 } 642 643 public function dismiss_set($sessionid) { 644 if (!empty($this->conn_sets[$sessionid])) { 645 foreach ($this->conn_sets[$sessionid] as $handle) { 646 // Since we want to dismiss this, don't generate any errors if it's dead already. 647 @socket_shutdown($handle); 648 @socket_close($handle); 649 } 650 } 651 $chatroom = $this->sets_info[$sessionid]['chatid']; 652 $userid = $this->sets_info[$sessionid]['userid']; 653 unset($this->conn_sets[$sessionid]); 654 unset($this->sets_info[$sessionid]); 655 unset($this->chatrooms[$chatroom]['users'][$sessionid]); 656 $this->trace('Removed all traces of user with session '.$sessionid, E_USER_NOTICE); 657 return true; 658 } 659 660 public function dismiss_ufo($handle, $disconnect = true, $message = null) { 661 if (empty($this->conn_ufo)) { 662 return false; 663 } 664 foreach ($this->conn_ufo as $id => $ufo) { 665 if ($ufo->handle == $handle) { 666 unset($this->conn_ufo[$id]); 667 if ($disconnect) { 668 if (!empty($message)) { 669 $this->write_data($handle, $message."\n\n"); 670 } 671 socket_shutdown($handle); 672 socket_close($handle); 673 } 674 return true; 675 } 676 } 677 return false; 678 } 679 680 public function conn_accept() { 681 $readsocket = array($this->listen_socket); 682 $write = null; 683 $except = null; 684 $changed = socket_select($readsocket, $write, $except, 0, 0); 685 686 if (!$changed) { 687 return false; 688 } 689 $handle = socket_accept($this->listen_socket); 690 if (!$handle) { 691 return false; 692 } 693 694 $newconn = new ChatConnection($handle); 695 $id = $this->new_ufo_id(); 696 $this->conn_ufo[$id] = $newconn; 697 } 698 699 public function conn_activity_ufo(&$handles) { 700 $monitor = array(); 701 if (!empty($this->conn_ufo)) { 702 foreach ($this->conn_ufo as $ufoid => $ufo) { 703 $monitor[$ufoid] = $ufo->handle; 704 } 705 } 706 707 if (empty($monitor)) { 708 $handles = array(); 709 return 0; 710 } 711 712 $a = null; 713 $b = null; 714 $retval = socket_select($monitor, $a, $b, null); 715 $handles = $monitor; 716 717 return $retval; 718 } 719 720 public function message_broadcast($message, $sender) { 721 722 if (empty($this->conn_sets)) { 723 return true; 724 } 725 726 $now = time(); 727 728 // First of all, mark this chatroom as having had activity now. 729 $this->chatrooms[$message->chatid]['lastactivity'] = $now; 730 731 foreach ($this->sets_info as $sessionid => $info) { 732 // We need to get handles from users that are in the same chatroom, same group. 733 if ($info['chatid'] == $message->chatid && 734 ($info['groupid'] == $message->groupid || $message->groupid == 0)) { 735 736 // Simply give them the message. 737 $output = chat_format_message_manually($message, $info['courseid'], $sender, $info['user']); 738 $this->trace('Delivering message "'.$output->text.'" to '.$this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL]); 739 740 if ($output->beep) { 741 $this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], 742 '<embed src="'.$this->_beepsoundsrc.'" autostart="true" hidden="true" />'); 743 } 744 745 if ($info['quirks'] & QUIRK_CHUNK_UPDATE) { 746 $output->html .= $GLOBALS['CHAT_DUMMY_DATA']; 747 $output->html .= $GLOBALS['CHAT_DUMMY_DATA']; 748 $output->html .= $GLOBALS['CHAT_DUMMY_DATA']; 749 } 750 751 if (!$this->write_data($this->conn_sets[$sessionid][CHAT_CONNECTION_CHANNEL], $output->html)) { 752 $this->disconnect_session($sessionid); 753 } 754 } 755 } 756 } 757 758 public function disconnect_session($sessionid) { 759 global $DB; 760 761 $info = $this->sets_info[$sessionid]; 762 763 $DB->delete_records('chat_users', array('sid' => $sessionid)); 764 $msg = new stdClass; 765 $msg->chatid = $info['chatid']; 766 $msg->userid = $info['userid']; 767 $msg->groupid = $info['groupid']; 768 $msg->system = 1; 769 $msg->message = 'exit'; 770 $msg->timestamp = time(); 771 772 $this->trace('User has disconnected, destroying uid '.$info['userid'].' with SID '.$sessionid, E_USER_WARNING); 773 chat_send_chatmessage($info['chatuser'], $msg->message, true); 774 775 // IMPORTANT, kill him BEFORE broadcasting, otherwise we 'll get infinite recursion! 776 $latesender = $info['user']; 777 $this->dismiss_set($sessionid); 778 $this->message_broadcast($msg, $latesender); 779 } 780 781 public function fatal($message) { 782 $message .= "\n"; 783 if ($this->_logfile) { 784 $this->trace($message, E_USER_ERROR); 785 } 786 echo "FATAL ERROR:: $message\n"; 787 die(); 788 } 789 790 public function init_sockets() { 791 global $CFG; 792 793 $this->trace('Setting up sockets'); 794 795 if (false === ($this->listen_socket = socket_create(AF_INET, SOCK_STREAM, 0))) { 796 // Failed to create socket. 797 $lasterr = socket_last_error(); 798 $this->fatal('socket_create() failed: '. socket_strerror($lasterr).' ['.$lasterr.']'); 799 } 800 801 if (!socket_bind($this->listen_socket, $CFG->chat_serverip, $CFG->chat_serverport)) { 802 // Failed to bind socket. 803 $lasterr = socket_last_error(); 804 $this->fatal('socket_bind() failed: '. socket_strerror($lasterr).' ['.$lasterr.']'); 805 } 806 807 if (!socket_listen($this->listen_socket, $CFG->chat_servermax)) { 808 // Failed to get socket to listen. 809 $lasterr = socket_last_error(); 810 $this->fatal('socket_listen() failed: '. socket_strerror($lasterr).' ['.$lasterr.']'); 811 } 812 813 // Socket has been initialized and is ready. 814 $this->trace('Socket opened on port '.$CFG->chat_serverport); 815 816 // What exactly does this do? http://www.unixguide.net/network/socketfaq/4.5.shtml is still not enlightening enough for me. 817 socket_set_option($this->listen_socket, SOL_SOCKET, SO_REUSEADDR, 1); 818 socket_set_nonblock($this->listen_socket); 819 } 820 821 public function cli_switch($switch, $param = null) { 822 switch($switch) { // LOL! 823 case 'reset': 824 // Reset sockets. 825 $this->_resetsocket = true; 826 return false; 827 case 'start': 828 // Start the daemon. 829 $this->_readytogo = true; 830 return false; 831 break; 832 case 'v': 833 // Verbose mode. 834 $this->_trace_level = E_ALL; 835 return false; 836 break; 837 case 'l': 838 // Use logfile. 839 if (!empty($param)) { 840 $this->_logfile_name = $param; 841 } 842 $this->_logfile = @fopen($this->_logfile_name, 'a+'); 843 if ($this->_logfile == false) { 844 $this->fatal('Failed to open '.$this->_logfile_name.' for writing'); 845 } 846 return false; 847 default: 848 // Unrecognized. 849 $this->fatal('Unrecognized command line switch: '.$switch); 850 break; 851 } 852 return false; 853 } 854 855 } 856 857 $daemon = new ChatDaemon; 858 set_error_handler(array($daemon, 'error_handler')); 859 860 // Check the parameters. 861 862 unset($argv[0]); 863 $commandline = implode(' ', $argv); 864 if (strpos($commandline, '-') === false) { 865 if (!empty($commandline)) { 866 // We cannot have received any meaningful parameters. 867 $daemon->fatal('Garbage in command line'); 868 } 869 } else { 870 // Parse command line. 871 $switches = preg_split('/(-{1,2}[a-zA-Z]+) */', $commandline, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); 872 873 // Taking advantage of the fact that $switches is indexed with incrementing numeric keys. 874 // We will be using that to pass additional information to those switches who need it. 875 $numswitches = count($switches); 876 877 // Fancy way to give a "hyphen" boolean flag to each "switch". 878 $switches = array_map(create_function('$x', 'return array("str" => $x, "hyphen" => (substr($x, 0, 1) == "-"));'), $switches); 879 880 for ($i = 0; $i < $numswitches; ++$i) { 881 882 $switch = $switches[$i]['str']; 883 $params = ($i == $numswitches - 1 ? null : 884 ($switches[$i + 1]['hyphen'] ? null : trim($switches[$i + 1]['str'])) 885 ); 886 887 if (substr($switch, 0, 2) == '--') { 888 // Double-hyphen switch. 889 $daemon->cli_switch(strtolower(substr($switch, 2)), $params); 890 } else if (substr($switch, 0, 1) == '-') { 891 // Single-hyphen switch(es), may be more than one run together. 892 $switch = substr($switch, 1); // Get rid of the "-". 893 $len = strlen($switch); 894 for ($j = 0; $j < $len; ++$j) { 895 $daemon->cli_switch(strtolower(substr($switch, $j, 1)), $params); 896 } 897 } 898 } 899 } 900 901 if (!$daemon->query_start()) { 902 // For some reason we didn't start, so print out some info. 903 echo 'Starts the Moodle chat socket server on port '.$CFG->chat_serverport; 904 echo "\n\n"; 905 echo "Usage: chatd.php [parameters]\n\n"; 906 echo "Parameters:\n"; 907 echo " --start Starts the daemon\n"; 908 echo " -v Verbose mode (prints trivial information messages)\n"; 909 echo " -l [logfile] Log all messages to logfile (if not specified, chatd.log)\n"; 910 echo "Example:\n"; 911 echo " chatd.php --start -l\n\n"; 912 die(); 913 } 914 915 if (!function_exists('socket_set_option')) { 916 echo "Error: Function socket_set_option() does not exist.\n"; 917 echo "Possibly PHP has not been compiled with --enable-sockets.\n\n"; 918 die(); 919 } 920 921 $daemon->init_sockets(); 922 923 $daemon->trace('Started Moodle chatd on port '.$CFG->chat_serverport.', listening socket '.$daemon->listen_socket, E_USER_WARNING); 924 925 // Clear the decks of old stuff. 926 $DB->delete_records('chat_users', array('version' => 'sockets')); 927 928 while (true) { 929 $active = array(); 930 931 // First of all, let's see if any of our UFOs have identified itself. 932 if ($daemon->conn_activity_ufo($active)) { 933 foreach ($active as $handle) { 934 $readsocket = array($handle); 935 $write = null; 936 $except = null; 937 $changed = socket_select($readsocket, $write, $except, 0, 0); 938 939 if ($changed > 0) { 940 // Let's see what it has to say. 941 942 $data = socket_read($handle, 2048); // Should be more than 512 to prevent empty pages and repeated messages! 943 if (empty($data)) { 944 continue; 945 } 946 947 if (strlen($data) == 2048) { // If socket_read has more data, ignore all data. 948 $daemon->trace('UFO with '.$handle.': Data too long; connection closed', E_USER_WARNING); 949 $daemon->dismiss_ufo($handle, true, 'Data too long; connection closed'); 950 continue; 951 } 952 953 if (!preg_match('/win=(chat|users|message|beep).*&chat_sid=([a-zA-Z0-9]*) HTTP/', $data, $info)) { 954 // Malformed data. 955 $daemon->trace('UFO with '.$handle.': Request with malformed data; connection closed', E_USER_WARNING); 956 $daemon->dismiss_ufo($handle, true, 'Request with malformed data; connection closed'); 957 continue; 958 } 959 960 $type = $info[1]; 961 $sessionid = $info[2]; 962 963 $customdata = array(); 964 965 switch($type) { 966 case 'chat': 967 $type = CHAT_CONNECTION_CHANNEL; 968 $customdata['quirks'] = 0; 969 if (strpos($data, 'Safari')) { 970 $daemon->trace('Safari identified...', E_USER_WARNING); 971 $customdata['quirks'] += QUIRK_CHUNK_UPDATE; 972 } 973 break; 974 case 'users': 975 $type = CHAT_SIDEKICK_USERS; 976 break; 977 case 'beep': 978 $type = CHAT_SIDEKICK_BEEP; 979 if (!preg_match('/beep=([^&]*)[& ]/', $data, $info)) { 980 $daemon->trace('Beep sidekick did not contain a valid userid', E_USER_WARNING); 981 $daemon->dismiss_ufo($handle, true, 'Request with malformed data; connection closed'); 982 continue; 983 } else { 984 $customdata = array('beep' => intval($info[1])); 985 } 986 break; 987 case 'message': 988 $type = CHAT_SIDEKICK_MESSAGE; 989 if (!preg_match('/chat_message=([^&]*)[& ]chat_msgidnr=([^&]*)[& ]/', $data, $info)) { 990 $daemon->trace('Message sidekick did not contain a valid message', E_USER_WARNING); 991 $daemon->dismiss_ufo($handle, true, 'Request with malformed data; connection closed'); 992 continue; 993 } else { 994 $customdata = array('message' => $info[1], 'index' => $info[2]); 995 } 996 break; 997 default: 998 $daemon->trace('UFO with '.$handle.': Request with unknown type; connection closed', E_USER_WARNING); 999 $daemon->dismiss_ufo($handle, true, 'Request with unknown type; connection closed'); 1000 continue; 1001 break; 1002 } 1003 1004 // OK, now we know it's something good. Promote it and pass it all the data it needs. 1005 $daemon->promote_ufo($handle, $type, $sessionid, $customdata); 1006 continue; 1007 } 1008 } 1009 } 1010 1011 $now = time(); 1012 1013 // Clean up chatrooms with no activity as required. 1014 if ($now - $daemon->_last_idle_poll >= $daemon->_freq_poll_idle_chat) { 1015 $daemon->poll_idle_chats($now); 1016 } 1017 1018 // Finally, accept new connections. 1019 $daemon->conn_accept(); 1020 1021 usleep($daemon->_time_rest_socket); 1022 } 1023 1024 @socket_shutdown($daemon->listen_socket, 0); 1025 die("\n\n-- terminated --\n"); 1026
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |