Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/mod/chat/ -> chatd.php (source)

Differences Between: [Versions 311 and 402] [Versions 311 and 403]

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