PageRenderTime 59ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/mod/chat/chatd.php

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