Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
/mod/chat/ -> chatd.php (source)

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

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