Subversion Repositories cryptochat

Rev

Rev 3 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. // ViaThinkSoft CryptoChat
  4. // (C) 2014-2018 by Daniel Marschall, ViaThinkSoft
  5. // Licensed under the terms of GPLv3
  6.  
  7. // -----------------------------------------------------------------------------------------------
  8.  
  9. // feature: user comes online, user comes offline, user is inactive, user is active, user is typing, get user list
  10. // todo: hide password length to avoid screensaves revealing confidential information
  11. // feature: split screen (frames), with united document.title ! (sum unread posts of each chat)
  12. // ng: smileys
  13. // ng: roomname, username encrypt
  14. // ng: sha3
  15. // ng: maybe it would be usefil if you could see by different colors if something was encrypted using a different password
  16. // bug: some browsers cannot mark the last line, otherwise everything will be marked
  17. // bug: offline detection does not work...
  18. // todo: "reset chat" (counter=0, document='') while pwd change?
  19. // feature: verhindern dass man etwas postet ohne ein passwort eingegeben zu haben? -> config
  20. // feature: posts durchnumerieren?
  21. // bug openpandora: password field on change geht net (cannot repro all the time?) ist !== null ok? (fixed now?)
  22. // bug cheery laptop: froze, didn't check new messages (mehrfach. repro? Aurora 24.2.0 Gentoo). eigene kommen nicht an. nach einer weile geht es plötzlich wieder? evtl ist vts request 1 mal fehlgeschlagen. (fixed now with fail callbacks?)
  23. // feature: shift+enter = new line without sending?
  24. // rp feature: ((->[i] , rp notizen + cors
  25. // feature: send files?
  26. // feature: dereferer
  27. // feature: configuration option to show a drop down box with all chat rooms
  28. // todo: always renew cache
  29. // todo: warn if config is not correct, e.g. admin password missing
  30. // feature: prevent making screenshots? keep clipboard empty?
  31. // feature: detect NoScript
  32. // todo: onChatHide() does not get called when tab is active (and visible), but browser window hasn't the focus
  33. // feature: loginmessage. show version of user. user soll bei jedem ajax-verkehr die version mitsenden, damit der server ihn ablehnen kann wenn die version zu alt ist!. ggf ein re-load forcing (self.reload in same room/user/pwd)?
  34. // idea: undo decryption if pwd field changed? delete pwd field and save actual decryption key in VAR array. so no screenloggers can see
  35. // feature?: possibility to disable hash? just try to do AES.
  36. // feature?: possibility to disable user stats or to prune them.
  37. // feature?: possibility to prune chat
  38. // feature?: send pwd hashed with refresh() so you can see which key(s) the active users use?
  39. // feature?: at "(Logging off)"-Message sending: remove DIRECTLY from stat list
  40. // feature: when configuration symbols are not defined, redefine them with default settings
  41. // feature: online list - instead of showing 2 same names, just write "x2"
  42.  
  43. // -----------------------------------------------------------------------------------------------
  44.  
  45. if (!file_exists(__DIR__ . '/config/config.inc.php')) {
  46.         die('ERROR: File <b>config/config.inc.php</b> does not exist. Please create it using <b>config/config.original.inc.php</b>');
  47. }
  48. require __DIR__ . '/config/config.inc.php';
  49. define('PRODUCT_NAME', 'CryptoChat');
  50. define('MCC_VER',      trim(file_get_contents(__DIR__ . '/VERSION')));
  51.  
  52. require __DIR__ . '/' . DEP_DIR_SAJAX       . '/php/sajax.php';
  53. require __DIR__ . '/' . DEP_DIR_JSONWRAPPER . '/jsonwrapper.php'; // TODO: make optional. only required if running with PHP 5.1
  54.  
  55. header('Content-type: text/html; charset=utf-8');
  56.  
  57. if ((REQUIRE_TLS) && (!is_secure())) {
  58.         // TODO: redirect
  59.         echo '<h1>Please connect via HTTPS or change <b>REQUIRE_TLS</b> to false.</h1>';
  60.         die();
  61. }
  62.  
  63. define('ACTION_LIST_ROOMS',  1);
  64. define('ACTION_CREATE_ROOM', 2);
  65.  
  66. $ses_room  = (isset($_POST['ses_room']))  ? trim($_POST['ses_room'])  : '';
  67. $ses_user  = (isset($_POST['ses_user']))  ? trim($_POST['ses_user'])  : '';
  68. $admin_pwd = (isset($_POST['admin_pwd'])) ? trim($_POST['admin_pwd']) : '';
  69.  
  70. $ses_room = trim(strtolower($ses_room));
  71. $ses_user = trim($ses_user);
  72. // $admin_pwd will be trimmed in adminAuth()
  73.  
  74. $chat_exists = (!CHAT_MUST_EXIST) || (adminAuth($admin_pwd, ACTION_CREATE_ROOM)) || chat_exists($ses_room);
  75.  
  76. $is_logged_in = ($ses_room != '') && ($ses_user != '') && $chat_exists;
  77. $show_list    = isset($_REQUEST['list_chatrooms']) && (!LIST_CHATS_REQUIRE_PWD || adminAuth($_POST['admin_pwd'], ACTION_LIST_ROOMS));
  78.  
  79. if ($is_logged_in) {
  80.         $chatroom_file = chatroom_file($ses_room);
  81.         touch($chatroom_file);
  82. }
  83.  
  84. function adminAuth($password, $action) {
  85.         if ($action == ACTION_LIST_ROOMS) {
  86.                 // if (trim(PWD_LIST_ROOMS) == '') return false;
  87.                 return (trim($password) == trim(PWD_LIST_ROOMS));
  88.         } else if ($action == ACTION_CREATE_ROOM) {
  89.                 // if (trim(PWD_CREATE_ROOM) == '') return false;
  90.                 return (trim($password) == trim(PWD_CREATE_ROOM));
  91.         } else {
  92.                 throw new Exception('Unknown action');
  93.         }
  94. }
  95.  
  96. function is_secure() {
  97.         // https://stackoverflow.com/questions/1175096/how-to-find-out-if-you-are-using-https-without-serverhttps
  98.         if(isset($_SERVER['HTTPS'])) {
  99.                 if ($_SERVER['HTTPS'] == 'on') {
  100.                         return true;
  101.                 }
  102.         }
  103.         return false;
  104. }
  105.  
  106. function chat_exists($room) {
  107.         return file_exists(chatroom_file($room));
  108. }
  109.  
  110. function chatroom_file($room) {
  111.         return __DIR__ . '/chats/'.escape_filename($room).'.html';
  112. }
  113.  
  114. function chatroom_userstat_file($room, $unique_id) {
  115.         return __DIR__ . '/chats/'.escape_filename($room).'_'.escape_filename($unique_id).'.session';
  116. }
  117.  
  118. function chatroom_userstat_files($room) {
  119.         $ses_ids = array();
  120.  
  121.         $ary = glob(__DIR__ . '/chats/'.escape_filename($room).'_*.session');
  122.         foreach ($ary as &$a) {
  123.                 if (!preg_match('@'.preg_quote(__DIR__ . '/chats/'.escape_filename($room).'_').'(.+)\.session@ismU', $a, $m)) {
  124.                         die('Internal error at '.__LINE__);
  125.                 }
  126.                 $ses_ids[] = $m[1];
  127.         }
  128.  
  129.         return $ses_ids;
  130. }
  131.  
  132. function write_user_stat($room, $unique_id, $username, $ip) {
  133.         $file = chatroom_userstat_file($room, $unique_id);
  134.  
  135.         $cont  = '';
  136. #       $cont .= "ROOM:$room\n";
  137.         $cont .= "USERNAME:$username\n";
  138. #       $cont .= "UNIQUEID:$unique_id\n";
  139.         $cont .= "IP:$ip\n";
  140. #       $now=time(); $cont .= "ACTIVE:$now\n";
  141. #       $key=...; $cont .= "KEY:$key\n";
  142.  
  143.         file_put_contents($file, $cont);
  144. }
  145.  
  146. function file_age($file) {
  147.         return time()-filemtime($file);
  148. }
  149.  
  150. function get_active_users($room, $max_inactivity=USERSTAT_INACTIVE_SECS) {
  151.         $ses_ids = array();
  152.  
  153.         $all_ses_ids = chatroom_userstat_files($room);
  154.         foreach ($all_ses_ids as &$ses_id) {
  155.                 $file = chatroom_userstat_file($room, $ses_id);
  156.                 if (file_age($file) <= $max_inactivity) {
  157.                         $ses_ids[] = $ses_id;
  158.                 } else {
  159.                         if (DELETE_OLD_USERSTATS) {
  160.                                 $file = chatroom_userstat_file($room, $ses_id);
  161.                                 unlink($file);
  162.                         }
  163.                 }
  164.         }
  165.  
  166.         return $ses_ids;
  167. }
  168.  
  169. function read_user_stat($room, $unique_id) {
  170.         $res = array();
  171.  
  172.         $file = chatroom_userstat_file($room, $unique_id);
  173.  
  174.         if (!file_exists($file)) return false;
  175.  
  176.         $cont = file($file);
  177.         foreach ($cont as &$c) {
  178.                 $c = trim($c);
  179.                 if ($c == '') continue;
  180.                 $nameval = explode(':', $c, 2);
  181.                 if (count($nameval) < 2) continue;
  182.  
  183.                 $name = $nameval[0];
  184.                 $val  = $nameval[1];
  185.                 $res[$name] = $val;
  186.         }
  187.         $res['ACTIVE']   = filemtime($file);
  188.         $res['ROOM']     = $room;
  189.         $res['UNIQUEID'] = $unique_id;
  190.  
  191.         return $res;
  192. }
  193.  
  194. function list_rooms() {
  195.         $ary = glob(__DIR__ . '/chats/*.html');
  196.         foreach ($ary as &$a) {
  197.                 $a = basename($a, '.html');
  198.         }
  199.         return $ary;
  200. }
  201.  
  202. function count_messages($room) {
  203.         $room_file = chatroom_file($room);
  204.         return count(file($room_file));
  205. }
  206.  
  207. function count_users($room) {
  208.         $room_file = chatroom_file($room);
  209.         $cont = file_get_contents($room_file);
  210.         preg_match_all('@\(user:(.*)\)@ismU', $cont, $m);
  211.         foreach ($m[1] as &$us) {
  212.                 $users_lookup[$us] = true;
  213.         }
  214.         return count($users_lookup);
  215. }
  216.  
  217. function escape_filename($str) {
  218.         $str = str_replace('/', '_', $str);
  219.         return $str;
  220. }
  221.  
  222. /* exported via AJAX */
  223. function add_line($id, $room, $user, $msg) {
  224.         if (!chat_exists($room)) return;
  225.         if ($user == '') return;
  226.  
  227.         $f = fopen(chatroom_file($room), 'a');
  228.         $dt = date('Y-m-d H:i:s');
  229. //      $msg = stripslashes($msg);
  230.         $remote = $_SERVER['REMOTE_ADDR'];
  231.  
  232.         // Escape stuff
  233.         $user = htmlentities($user, ENT_QUOTES, 'UTF-8');
  234.         $msg  = htmlentities($msg,  ENT_QUOTES, 'UTF-8');
  235.  
  236.         $msgs = explode("\n", $msg);
  237.         foreach ($msgs as &$msg) {
  238.                 fwrite($f, "(date: $dt)(user: $user)$msg<br>\n");
  239.         }
  240.  
  241.         fclose($f);
  242.  
  243.         return array($id);
  244. }
  245.  
  246. /* exported via AJAX */
  247. function refresh($room, $fromline, $username, $unique_id) {
  248.         if (!chat_exists($room)) return;
  249.         if ($fromline < 0) return;
  250.  
  251.         $unique_id = trim($unique_id);
  252.         if ($unique_id == '') {
  253.                 # TODO: show error message
  254.                 return false;
  255.         }
  256.         write_user_stat($room, $unique_id, $username, $_SERVER['REMOTE_ADDR']);
  257.  
  258.         $res = '';
  259.         $lines = file(chatroom_file($room));
  260.         for ($i=$fromline; $i<count($lines); $i++) {
  261.                 $res .= $lines[$i] . "\n";
  262.         }
  263.  
  264.         $userstats = array();
  265.         $ses_ids = get_active_users($room);
  266.         foreach ($ses_ids as &$ses_id) {
  267.                 $userstats[] = read_user_stat($room, $ses_id);
  268.         }
  269.  
  270.         return array(count($lines), $res, $userstats);
  271. }
  272.  
  273. // $sajax_debug_mode = true;
  274. $sajax_failure_redirect = 'http://web.archive.org/web/20090915191608/http://sajax.info/sajaxfail.html';
  275. sajax_export(
  276.         array('name' => 'add_line', 'method' => 'POST'),
  277.         array('name' => 'refresh',  'method' => 'GET')   // TODO: post?
  278. );
  279. sajax_handle_client_request();
  280.  
  281. ?>
  282. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  283.         "http://www.w3.org/TR/html4/loose.dtd">
  284. <html>
  285. <head>
  286.         <META HTTP-EQUIV="content-type" CONTENT="text/html; charset=utf-8">
  287.         <title><?php echo htmlentities(PRODUCT_NAME); ?></title>
  288.         <style>
  289.         body {
  290.                 background-color:#000011;
  291.                 color:white;
  292.                 font-family:Arial;
  293.                 font-size:13px;
  294.         }
  295.         body.chatroom {
  296.                 margin: 0;
  297.                 background-color:#000011;
  298.                 color:white;
  299.                 font-family:Arial;
  300.                 font-size:13px;
  301.         }
  302.         thead td {
  303.                 font-weight:bold;
  304.                 background-color:#000044;
  305.         }
  306.         .encrypted {
  307.                 color:orange;
  308.         }
  309.         .error {
  310.                 color:#dd0000;
  311.         }
  312.         input, textarea {
  313.                 background-color:#000011;
  314.                 color:white;
  315.         }
  316.         #postarea textarea {
  317.                 width:80%;
  318.                 background-color:#000011;
  319.                 color:white;
  320.         }
  321.         #header {
  322.                 padding:5px;
  323.                 position:fixed;
  324.                 background-color:#000044;
  325.                 color:white;
  326.                 width:100%;
  327.                 height: 50px;
  328.         }
  329.         #status {
  330.                 position:fixed;
  331.                 right:5px;
  332.                 top:5px;
  333.         }
  334.         #chatmessages {
  335.                 padding-bottom:115px;
  336.                 padding-top: 65px;
  337.                 padding-left: 5px;
  338.                 padding-right: 5px;
  339.         }
  340.         #activeusers {
  341.                 position:fixed;
  342.                 top:65px;
  343.                 /*bottom:115px;*/
  344.                 right:5px;
  345.                 /*height:100px;*/
  346.                 width:150px;
  347.                 background-color:#222222;
  348.                 padding:10px;
  349.         }
  350.         #postarea {
  351.                 padding:5px;
  352.                 position: fixed;
  353.                 bottom:0;
  354.                 height: 100px;
  355.                 width: 100%;
  356.                 background-color:#000044;
  357.                 color:white;
  358.         }
  359.         .button {
  360.                 /* http://www.drweb.de/magazin/browser-cursor-hand/ */
  361.                 cursor: hand; cursor: pointer;
  362.                 font-family: Arial;
  363.                 color:white;
  364.                 background-color: #000022;
  365.                 border-color:#000000;
  366.                 border-style: inset;
  367.         }
  368.         .button:hover {
  369.                 color:yellow;
  370.         }
  371.         label {
  372.                 /* http://www.drweb.de/magazin/browser-cursor-hand/ */
  373.                 cursor: hand; cursor: pointer;
  374.         }
  375.         label:hover {
  376.                 color:yellow;
  377.         }
  378.         a:link {
  379.                 color:#ffdd00;
  380.         }
  381.         a:visited {
  382.                 color:darkred;
  383.         }
  384.         a:active, a:hover {
  385.                 color:white;
  386.         }
  387.         .logout a {
  388.                 color:white;
  389.         }
  390.         .logout a:hover {
  391.                 color:orange;
  392.         }
  393.         .logout {
  394.                 margin-left: 20px;
  395.         }
  396.         </style>
  397.  
  398. <?php
  399. if ($is_logged_in) {
  400. ?>
  401.  
  402.         <script type="text/javascript" src="<?php echo DEP_DIR_CRYPTOJS; ?>/rollups/sha256.js"></script>
  403.         <script type="text/javascript" src="<?php echo DEP_DIR_CRYPTOJS; ?>/rollups/aes.js"></script>
  404.         <script type="text/javascript" src="<?php echo DEP_DIR_CRYPTOJS; ?>/rollups/md5.js"></script>
  405.  
  406.         <script type="text/javascript" src="<?php echo DEP_DIR_SAJAX; ?>/php/json_stringify.js"></script>
  407.         <script type="text/javascript" src="<?php echo DEP_DIR_SAJAX; ?>/php/json_parse.js"></script>
  408.         <script type="text/javascript" src="<?php echo DEP_DIR_SAJAX; ?>/php/sajax.js"></script>
  409.  
  410.         <script type="text/javascript" src="<?php echo DEP_DIR_CRC32; ?>/crc32.js"></script>
  411.  
  412.         <script type="text/javascript">
  413.  
  414.         <?php
  415.         sajax_show_javascript();
  416.         ?>
  417.  
  418.         var product = <?php echo json_encode(PRODUCT_NAME); ?>;
  419.         var version = <?php echo json_encode(MCC_VER); ?>;
  420.         var room = <?php echo json_encode($ses_room); ?>;
  421.         var user = <?php echo json_encode($ses_user); ?>;
  422.         var curseq = 0;
  423.         var roomSalt = "<?php echo sha1(X_SALT.$ses_room); ?>";
  424.         var curVisible = true;
  425.         var backgroundSeqCount = 0;
  426.         var blink = 0;
  427.         var uniqueID = "";
  428.  
  429.         // --- Useful functions
  430.  
  431.         function scrollToBottom() {
  432.                 // http://www.sourcetricks.com/2010/07/javascript-scroll-to-bottom-of-page.html
  433.                 window.scrollTo(0, document.body.scrollHeight);
  434.         }
  435.  
  436.         function randNum(min, max) {
  437.                 return Math.floor((Math.random()*max)+min);
  438.         }
  439.  
  440.         // http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript/1349426#1349426
  441.         function randomString(len) {
  442.                 var text = "";
  443.                 var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  444.  
  445.                 for( var i=0; i < len; i++ ) {
  446.                         text += possible.charAt(Math.floor(Math.random() * possible.length));
  447.                 }
  448.  
  449.                 return text;
  450.         }
  451.  
  452.         // Array Remove - By John Resig (MIT Licensed)
  453.         Array.prototype.remove = function(from, to) {
  454.                 var rest = this.slice((to || from) + 1 || this.length);
  455.                 this.length = from < 0 ? this.length + from : from;
  456.                 return this.push.apply(this, rest);
  457.         };
  458.  
  459.         function sleep(milliseconds) {
  460.                 // http://www.phpied.com/sleep-in-javascript/
  461.                 var start = new Date().getTime();
  462.                 for (var i = 0; i < 1e7; i++) {
  463.                         if ((new Date().getTime() - start) > milliseconds) {
  464.                                 break;
  465.                         }
  466.                 }
  467.         }
  468.  
  469.         function get_html_translation_table(table, quote_style) {
  470.                 // From: http://phpjs.org/functions
  471.                 // +   original by: Philip Peterson
  472.                 // +    revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  473.                 // +   bugfixed by: noname
  474.                 // +   bugfixed by: Alex
  475.                 // +   bugfixed by: Marco
  476.                 // +   bugfixed by: madipta
  477.                 // +   improved by: KELAN
  478.                 // +   improved by: Brett Zamir (http://brett-zamir.me)
  479.                 // +   bugfixed by: Brett Zamir (http://brett-zamir.me)
  480.                 // +      input by: Frank Forte
  481.                 // +   bugfixed by: T.Wild
  482.                 // +      input by: Ratheous
  483.                 // %          note: It has been decided that we're not going to add global
  484.                 // %          note: dependencies to php.js, meaning the constants are not
  485.                 // %          note: real constants, but strings instead. Integers are also supported if someone
  486.                 // %          note: chooses to create the constants themselves.
  487.                 // *     example 1: get_html_translation_table('HTML_SPECIALCHARS');
  488.                 // *     returns 1: {'"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;'}
  489.                 var entities = {},
  490.                         hash_map = {},
  491.                         decimal;
  492.                 var constMappingTable = {},
  493.                         constMappingQuoteStyle = {};
  494.                 var useTable = {},
  495.                         useQuoteStyle = {};
  496.  
  497.                 // Translate arguments
  498.                 constMappingTable[0] = 'HTML_SPECIALCHARS';
  499.                 constMappingTable[1] = 'HTML_ENTITIES';
  500.                 constMappingQuoteStyle[0] = 'ENT_NOQUOTES';
  501.                 constMappingQuoteStyle[2] = 'ENT_COMPAT';
  502.                 constMappingQuoteStyle[3] = 'ENT_QUOTES';
  503.  
  504.                 useTable = !isNaN(table) ? constMappingTable[table] : table ? table.toUpperCase() : 'HTML_SPECIALCHARS';
  505.                 useQuoteStyle = !isNaN(quote_style) ? constMappingQuoteStyle[quote_style] : quote_style ? quote_style.toUpperCase() : 'ENT_COMPAT';
  506.  
  507.                 if (useTable !== 'HTML_SPECIALCHARS' && useTable !== 'HTML_ENTITIES') {
  508.                         throw new Error("Table: " + useTable + ' not supported');
  509.                         // return false;
  510.                 }
  511.  
  512.                 entities['38'] = '&amp;';
  513.                 if (useTable === 'HTML_ENTITIES') {
  514.                         entities['160'] = '&nbsp;';
  515.                         entities['161'] = '&iexcl;';
  516.                         entities['162'] = '&cent;';
  517.                         entities['163'] = '&pound;';
  518.                         entities['164'] = '&curren;';
  519.                         entities['165'] = '&yen;';
  520.                         entities['166'] = '&brvbar;';
  521.                         entities['167'] = '&sect;';
  522.                         entities['168'] = '&uml;';
  523.                         entities['169'] = '&copy;';
  524.                         entities['170'] = '&ordf;';
  525.                         entities['171'] = '&laquo;';
  526.                         entities['172'] = '&not;';
  527.                         entities['173'] = '&shy;';
  528.                         entities['174'] = '&reg;';
  529.                         entities['175'] = '&macr;';
  530.                         entities['176'] = '&deg;';
  531.                         entities['177'] = '&plusmn;';
  532.                         entities['178'] = '&sup2;';
  533.                         entities['179'] = '&sup3;';
  534.                         entities['180'] = '&acute;';
  535.                         entities['181'] = '&micro;';
  536.                         entities['182'] = '&para;';
  537.                         entities['183'] = '&middot;';
  538.                         entities['184'] = '&cedil;';
  539.                         entities['185'] = '&sup1;';
  540.                         entities['186'] = '&ordm;';
  541.                         entities['187'] = '&raquo;';
  542.                         entities['188'] = '&frac14;';
  543.                         entities['189'] = '&frac12;';
  544.                         entities['190'] = '&frac34;';
  545.                         entities['191'] = '&iquest;';
  546.                         entities['192'] = '&Agrave;';
  547.                         entities['193'] = '&Aacute;';
  548.                         entities['194'] = '&Acirc;';
  549.                         entities['195'] = '&Atilde;';
  550.                         entities['196'] = '&Auml;';
  551.                         entities['197'] = '&Aring;';
  552.                         entities['198'] = '&AElig;';
  553.                         entities['199'] = '&Ccedil;';
  554.                         entities['200'] = '&Egrave;';
  555.                         entities['201'] = '&Eacute;';
  556.                         entities['202'] = '&Ecirc;';
  557.                         entities['203'] = '&Euml;';
  558.                         entities['204'] = '&Igrave;';
  559.                         entities['205'] = '&Iacute;';
  560.                         entities['206'] = '&Icirc;';
  561.                         entities['207'] = '&Iuml;';
  562.                         entities['208'] = '&ETH;';
  563.                         entities['209'] = '&Ntilde;';
  564.                         entities['210'] = '&Ograve;';
  565.                         entities['211'] = '&Oacute;';
  566.                         entities['212'] = '&Ocirc;';
  567.                         entities['213'] = '&Otilde;';
  568.                         entities['214'] = '&Ouml;';
  569.                         entities['215'] = '&times;';
  570.                         entities['216'] = '&Oslash;';
  571.                         entities['217'] = '&Ugrave;';
  572.                         entities['218'] = '&Uacute;';
  573.                         entities['219'] = '&Ucirc;';
  574.                         entities['220'] = '&Uuml;';
  575.                         entities['221'] = '&Yacute;';
  576.                         entities['222'] = '&THORN;';
  577.                         entities['223'] = '&szlig;';
  578.                         entities['224'] = '&agrave;';
  579.                         entities['225'] = '&aacute;';
  580.                         entities['226'] = '&acirc;';
  581.                         entities['227'] = '&atilde;';
  582.                         entities['228'] = '&auml;';
  583.                         entities['229'] = '&aring;';
  584.                         entities['230'] = '&aelig;';
  585.                         entities['231'] = '&ccedil;';
  586.                         entities['232'] = '&egrave;';
  587.                         entities['233'] = '&eacute;';
  588.                         entities['234'] = '&ecirc;';
  589.                         entities['235'] = '&euml;';
  590.                         entities['236'] = '&igrave;';
  591.                         entities['237'] = '&iacute;';
  592.                         entities['238'] = '&icirc;';
  593.                         entities['239'] = '&iuml;';
  594.                         entities['240'] = '&eth;';
  595.                         entities['241'] = '&ntilde;';
  596.                         entities['242'] = '&ograve;';
  597.                         entities['243'] = '&oacute;';
  598.                         entities['244'] = '&ocirc;';
  599.                         entities['245'] = '&otilde;';
  600.                         entities['246'] = '&ouml;';
  601.                         entities['247'] = '&divide;';
  602.                         entities['248'] = '&oslash;';
  603.                         entities['249'] = '&ugrave;';
  604.                         entities['250'] = '&uacute;';
  605.                         entities['251'] = '&ucirc;';
  606.                         entities['252'] = '&uuml;';
  607.                         entities['253'] = '&yacute;';
  608.                         entities['254'] = '&thorn;';
  609.                         entities['255'] = '&yuml;';
  610.                 }
  611.  
  612.                 if (useQuoteStyle !== 'ENT_NOQUOTES') {
  613.                         entities['34'] = '&quot;';
  614.                 }
  615.                 if (useQuoteStyle === 'ENT_QUOTES') {
  616.                         entities['39'] = '&#39;';
  617.                 }
  618.                 entities['60'] = '&lt;';
  619.                 entities['62'] = '&gt;';
  620.  
  621.                 // ascii decimals to real symbols
  622.                 for (decimal in entities) {
  623.                         if (entities.hasOwnProperty(decimal)) {
  624.                                 hash_map[String.fromCharCode(decimal)] = entities[decimal];
  625.                         }
  626.                 }
  627.  
  628.                 return hash_map;
  629.         }
  630.  
  631.         function htmlentities(string, quote_style, charset, double_encode) {
  632.                 // From: http://phpjs.org/functions
  633.                 // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  634.                 // +    revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  635.                 // +   improved by: nobbler
  636.                 // +    tweaked by: Jack
  637.                 // +   bugfixed by: Onno Marsman
  638.                 // +    revised by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
  639.                 // +    bugfixed by: Brett Zamir (http://brett-zamir.me)
  640.                 // +      input by: Ratheous
  641.                 // +   improved by: Rafa. Kukawski (http://blog.kukawski.pl)
  642.                 // +   improved by: Dj (http://phpjs.org/functions/htmlentities:425#comment_134018)
  643.                 // -    depends on: get_html_translation_table
  644.                 // *     example 1: htmlentities('Kevin & van Zonneveld');
  645.                 // *     returns 1: 'Kevin &amp; van Zonneveld'
  646.                 // *     example 2: htmlentities("foo'bar","ENT_QUOTES");
  647.                 // *     returns 2: 'foo&#039;bar'
  648.                 var hash_map = this.get_html_translation_table('HTML_ENTITIES', quote_style),
  649.                         symbol = '';
  650.                 string = string == null ? '' : string + '';
  651.  
  652.                 if (!hash_map) {
  653.                         return false;
  654.                 }
  655.  
  656.                 if (quote_style && quote_style === 'ENT_QUOTES') {
  657.                         hash_map["'"] = '&#039;';
  658.                 }
  659.  
  660.                 if (!!double_encode || double_encode == null) {         // TODO: korrekt "== null"? nicht "!= null"?
  661.                         for (symbol in hash_map) {
  662.                                 if (hash_map.hasOwnProperty(symbol)) {
  663.                                         string = string.split(symbol).join(hash_map[symbol]);
  664.                                 }
  665.                         }
  666.                 } else {
  667.                         string = string.replace(/([\s\S]*?)(&(?:#\d+|#x[\da-f]+|[a-zA-Z][\da-z]*);|$)/g, function (ignore, text, entity) {
  668.                                 for (symbol in hash_map) {
  669.                                         if (hash_map.hasOwnProperty(symbol)) {
  670.                                                 text = text.split(symbol).join(hash_map[symbol]);
  671.                                         }
  672.                                 }
  673.  
  674.                                 return text + entity;
  675.                         });
  676.                 }
  677.  
  678.                 return string;
  679.         }
  680.  
  681.         /* accepts parameters
  682.          * h  Object = {h:x, s:y, v:z}
  683.          * OR
  684.          * h, s, v
  685.          * This code expects 0 <= h, s, v <= 1
  686.          * http://stackoverflow.com/a/17243070
  687.         */
  688.         function HSVtoRGB(h, s, v) {
  689.                 var r, g, b, i, f, p, q, t;
  690.                 if (h && s === undefined && v === undefined) {
  691.                         s = h.s, v = h.v, h = h.h;
  692.                 }
  693.                 i = Math.floor(h * 6);
  694.                 f = h * 6 - i;
  695.                 p = v * (1 - s);
  696.                 q = v * (1 - f * s);
  697.                 t = v * (1 - (1 - f) * s);
  698.                 switch (i % 6) {
  699.                         case 0: r = v, g = t, b = p; break;
  700.                         case 1: r = q, g = v, b = p; break;
  701.                         case 2: r = p, g = v, b = t; break;
  702.                         case 3: r = p, g = q, b = v; break;
  703.                         case 4: r = t, g = p, b = v; break;
  704.                         case 5: r = v, g = p, b = q; break;
  705.                 }
  706.                 return {
  707.                         r: Math.floor(r * 255),
  708.                         g: Math.floor(g * 255),
  709.                         b: Math.floor(b * 255)
  710.                 };
  711.         }
  712.  
  713.         // Returns something between 0..255
  714.         function crc8(message) {
  715.                 // return parseInt(CryptoJS.SHA256(message).toString().substr(0,2), 16);
  716.                 // return parseInt(CryptoJS.MD5(message).toString().substr(0,2), 16);
  717.                 return crc32(message)%256;
  718.         }
  719.  
  720.         // http://stackoverflow.com/a/2998822
  721.         function pad(num, size) {
  722.                 var s = num+"";
  723.                 while (s.length < size) s = "0" + s;
  724.                 return s;
  725.         }
  726.  
  727.         function html_rgb(r, g, b) {
  728.                 return "#" + pad(r.toString(16), 2) + pad(g.toString(16), 2) + pad(b.toString(16), 2);
  729.  
  730.         }
  731.  
  732.         function getToken() {
  733.                 return new Date().getTime() + randNum(0, 999999);
  734.         }
  735.  
  736.         function replaceURLWithHTMLLinks(text) {
  737.                 // TODO: deferer?
  738.  
  739.                 // test@example.com
  740.                 text = text.replace(/mailto:(.+?)@/ig, "mailto:$1#");
  741.                 text = text.replace(/(([^>" ]+?)@([^<" ]+))/ig, "<a href=\"mailto:$1\" target=\"_blank\">$1</a>");
  742.                 text = text.replace(/mailto:(.+?)#/ig, "mailto:$1@");
  743.  
  744.                 // www.example.com
  745.                 text = text.replace(/:\/\/www\./ig, "://###.");
  746.                 text = text.replace(/\b(www\.(.+?))\b/ig, "http://$1");
  747.                 text = text.replace(/:\/\/###\./ig, "://www.");
  748.  
  749.                 // http://www.google.com
  750.                 // https://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
  751.                 var exp = /(\b(https?|ftp|file|mailto):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
  752.                 text = text.replace(/href="(.{1,5}?):/ig, "href=\"$1#");
  753.                 text = text.replace(exp, "<a href=\"$1\" target=\"_blank\">$1</a>");
  754.                 text = text.replace(/href="(.{1,5}?)#/ig, "href=\"$1:");
  755.  
  756.                 return text;
  757.         }
  758.  
  759.         function luminance_perceived(r, g, b) {
  760.                 // Luminance (standard, objective)
  761.                 // http://stackoverflow.com/a/596243
  762.                 // return (0.2126*r) + (0.7152*g) + (0.0722*b);
  763.  
  764.                 // Luminance (perceived option 1)
  765.                 // http://stackoverflow.com/a/596243
  766.                 // return (0.299*r) + (0.587*g) + (0.114*b);
  767.  
  768.                 // Luminance (perceived option 2, slower to calculate)
  769.                 // http://alienryderflex.com/hsp.html
  770.                 // return Math.sqrt(0.299*Math.pow(r,2) + 0.587*Math.pow(g,2) + 0.114*Math.pow(b,2))
  771.  
  772.                 // TODO: Is this the correct formula?
  773.                 return Math.sqrt(0.241*Math.pow(r,2) + 0.691*Math.pow(g,2) + 0.068*Math.pow(b,2))
  774.         }
  775.  
  776.         function colorizeUsername(username, salt) {
  777.                 if (salt === undefined) salt = "";
  778.  
  779.                 // return "#" + CryptoJS.SHA256(username).toString().substr(0,6);
  780.  
  781.                 // TODO: is there any method obtaining the background color in runtime? instead of using THEME_DARK
  782.  
  783.                 var x = crc8(username+salt);
  784.                 var rgb = HSVtoRGB(x/255, 1, 0.75);
  785.  
  786.                 var bg_lum = <?php if (THEME_DARK) echo 0 /* assume #000011 */; else echo 255 /* assume white */; ?>;
  787.                 var fg_lum = luminance_perceived(rgb.r, rgb.g, rgb.b);
  788.  
  789.                 var lum_dif = Math.floor(Math.abs(bg_lum-fg_lum));
  790.  
  791.                 if (lum_dif < 128) {
  792.                         <?php if (THEME_DARK) { ?>
  793.                         rgb.r = (rgb.r + (128-lum_dif)); if (rgb.r > 255) rgb.r = 255;
  794.                         rgb.g = (rgb.g + (128-lum_dif)); if (rgb.g > 255) rgb.g = 255;
  795.                         rgb.b = (rgb.b + (128-lum_dif)); if (rgb.b > 255) rgb.b = 255;
  796.                         <?php } else { ?>
  797.                         rgb.r = (rgb.r - (128-lum_dif)); if (rgb.r < 0) rgb.r = 0;
  798.                         rgb.g = (rgb.g - (128-lum_dif)); if (rgb.g < 0) rgb.g = 0;
  799.                         rgb.b = (rgb.b - (128-lum_dif)); if (rgb.b < 0) rgb.b = 0;
  800.                         <?php } ?>
  801.                 }
  802.  
  803.                 return html_rgb(rgb.r, rgb.g, rgb.b);
  804.         }
  805.  
  806.         // --- Getter and setter
  807.  
  808.         function encryptionEnabled() {
  809.                 return document.getElementById("doencrypt").checked == 1;
  810.         }
  811.  
  812.         function getPassword() {
  813.                 return document.getElementById("pwd").value.trim();
  814.         }
  815.  
  816.         function autoscrollEnabled() {
  817.                 return document.getElementById("autoscroll").checked == 1;
  818.         }
  819.  
  820.         // --- MD5
  821.  
  822.         var selftest_md5_finished = false;
  823.         function selftest_md5() {
  824.                 if (selftest_md5_finished) return;
  825.                 selftest_md5_finished = true;
  826.  
  827.                 var errors = "";
  828.                 cmp_a = CryptoJS.enc.Base64.stringify(CryptoJS.MD5("Message"));
  829.                 cmp_b = "TCqP5+ryRyHMep8BdRFb1A==";
  830.                 if (cmp_a !== cmp_b) {
  831.                         errors += "MD5 self test failed!\n";
  832.                         console.error("MD5 self test failed: '" + cmp_a + "' vs. '" + cmp_b + "'");
  833.                 }
  834.  
  835.                 if (errors) {
  836.                         alert(errors+"\nYour browser seems to be buggy. Decryption of particular messages might fail.");
  837.                 } else {
  838.                         console.info("MD5 self test passed");
  839.                 }
  840.         }
  841.  
  842.         function md5(message) {
  843.                 selftest_md5();
  844.                 var hash = CryptoJS.MD5(message);
  845.                 hash = CryptoJS.enc.Base64.stringify(hash);
  846.                 return hash;
  847.         }
  848.  
  849.         // --- SHA256
  850.  
  851.         var selftest_sha256_finished = false;
  852.         function selftest_sha256() {
  853.                 if (selftest_sha256_finished) return;
  854.                 selftest_sha256_finished = true;
  855.  
  856.                 var errors = "";
  857.                 cmp_a = CryptoJS.enc.Base64.stringify(CryptoJS.SHA256("Message"));
  858.                 cmp_b = "L3dmip37+NWEi57rSnFFypTG7ZI25Kdz9tyvpRMrL5E=";
  859.                 if (cmp_a !== cmp_b) {
  860.                         errors += "SHA256 self test failed!\n";
  861.                         console.error("SHA256 self test failed: '" + cmp_a + "' vs. '" + cmp_b + "'");
  862.                 }
  863.  
  864.                 if (errors) {
  865.                         alert(errors+"\nYour browser seems to be buggy. Decryption of particular messages might fail.");
  866.                 } else {
  867.                         console.info("SHA256 self test passed");
  868.                 }
  869.         }
  870.  
  871.         function sha256(message) {
  872.                 selftest_sha256();
  873.                 var hash = CryptoJS.SHA256(message);
  874.                 hash = CryptoJS.enc.Base64.stringify(hash);
  875.                 return hash;
  876.         }
  877.  
  878.         // --- AES
  879.  
  880.         var selftest_aes_finished = false;
  881.         function selftest_aes() {
  882.                 if (selftest_aes_finished) return;
  883.                 selftest_aes_finished = true;
  884.  
  885.                 var errors = "";
  886.                 var cmp_a = CryptoJS.AES.decrypt("U2FsdGVkX19kJJkA0NL7WJRdXKrdqDcf6A2yDODaL2g=", "Secret Passphrase").toString(CryptoJS.enc.Utf8);
  887.                 var cmp_b = "Message";
  888.                 if ((cmp_a !== cmp_b) || (aes_dec(aes_enc("Message")) !== "Message")) {
  889.                         errors += "AES self test failed!\n";
  890.                         console.error("AES self test failed: '" + cmp_a + "' vs. '" + cmp_b + "'");
  891.                 }
  892.  
  893.                 if (errors) {
  894.                         alert(errors+"\nYour browser seems to be buggy. Decryption of particular messages might fail.");
  895.                 } else {
  896.                         console.info("AES self test passed");
  897.                 }
  898.         }
  899.  
  900.         function aes_enc(msg, ver) {
  901.                 // ver is currently not used
  902.                 selftest_aes();
  903.                 var passwd = getPassword();
  904.                 return CryptoJS.AES.encrypt(msg, roomSalt+passwd);
  905.         }
  906.  
  907.         function aes_dec(msg, ver) {
  908.                 // ver is currently not used
  909.                 selftest_aes();
  910.                 var passwd = getPassword();
  911.                 try {
  912.                         return CryptoJS.AES.decrypt(msg, roomSalt+passwd).toString(CryptoJS.enc.Utf8);
  913.                 } catch (e) {
  914.                         return null;
  915.                 }
  916.         }
  917.  
  918.         // --- Decrypting stuff
  919.  
  920.         // Alternative hash digest for compatibility issues
  921.         // Some browsers like Jumanji have problems using SHA1 or SHA2, but work with MD5, e.g.
  922.         // Jumanji 1.1.2.4 (versionsangabe nicht sicher) ist inkompatibel mit CryptoJS 3.1.2
  923.         // UA = "Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/534.26+ (KHTML, like Gecko) Version/5.0 Safari/534.26+ jumanji/0.0"
  924.         // CryptoJS.MD5("Message"):
  925.         // - Normal:  4c2a8fe7eaf24721cc7a9f0175115bd4
  926.         // - Jumanji: 4c2a8fe7eaf24721cc7a9f0175115bd4 (OK)
  927.         // CryptoJS.SHA1("Message"):
  928.         // - Normal:  68f4145fee7dde76afceb910165924ad14cf0d00
  929.         // - Jumanji: 5a5aa74ecae1d696900b034d5f1b71497c170ea0 (Error)
  930.         // CryptoJS.SHA256("Message"):
  931.         // - Normal:  2f77668a9dfbf8d5848b9eeb4a7145ca94c6ed9236e4a773f6dcafa5132b2f91
  932.         // - Jumanji: 5a28f8b8778c15a166f7f17ebb89ce8e8381fbb5e39ddc2511239793119a649e (Error)
  933.         // CryptoJS.AES.encrypt("Message", "Secret Passphrase"):
  934.         // - Normal:  U2FsdGVkX19zlzNcfljComkcU0A7XfZ+gzZbI+GyFm0=
  935.         // - Jumanji: U2FsdGVkX19kJJkA0NL7WJRdXKrdqDcf6A2yDODaL2g= (OK)
  936.         // This is a fast version (4x MD5) necessary for slow computers with ARM architecture
  937.         function specialMD5fast4(message) {
  938.                 var a = md5("IuErOmVyPeL2ek6e16vkjTWjssgLmd" + message);
  939.                 var b = md5("8wxdm3mVi8UQXdboJvCctYwm8ZxTyX" + message);
  940.                 return md5(a+b) + md5(b+a);
  941.         }
  942.  
  943.         function specialHash(val, entrySalt, version) {
  944.                 var hash = null;
  945.                 if (version == 1) {
  946.                         hash = sha256(roomSalt+entrySalt+val);
  947.                 } else if (version == 2) {
  948.                         hash = specialMD5fast4(roomSalt+entrySalt+val);
  949.                 } else {
  950.                         console.error("Version " + version + " is unknown at specialHash()");
  951.                         return null;
  952.                 }
  953.                 return entrySalt + "_" + hash + "_ver" + version;
  954.         }
  955.  
  956.         function cbDecrypt(str, pEntrySalt, pHash, pVer, pMesg, offset, s) {
  957.                 var passwd = getPassword();
  958.  
  959.                 var hash_client = specialHash(passwd, pEntrySalt, pVer);
  960.                 var hash_server = pEntrySalt + "_" + pHash + "_ver" + pVer;
  961.                 if (hash_client == hash_server) {
  962.                         var msg = aes_dec(pMesg, pVer);
  963.                         msg = htmlentities(msg, "ENT_NOQUOTES", "UTF-8");
  964.                         return "<span class=\"encrypted\">" + msg + "</span>";
  965.                 } else {
  966.                         // TODO: maybe we can still make the string invisible or something, so it becomes more user comfortable?
  967.                         // TODO: maybe we can still save the decode-information so we can revert decryption as soon as the password is changed again?
  968.                         return str; // can be encrypted later
  969.                 }
  970.         }
  971.  
  972.         function cbUsername(str, pUsername, offset, s) {
  973.                 var color = colorizeUsername(pUsername);
  974.  
  975.                 return "<font color=\"" + color + "\"><b>[" + pUsername + "]</b></font>: ";
  976.         }
  977.  
  978.         function cbDate(str, pDateTime, offset, s) {
  979.                 return pDateTime + " - ";
  980.         }
  981.  
  982.         var formatAndDecryptIsRunning = false; // to prevent that two instances of formatAndDecrypt() run simultaneously
  983.         function formatAndDecrypt() {
  984.                 // Set the mutex and write a "Decrypting..." message
  985.                 //while (formatAndDecryptIsRunning);
  986.                 if (formatAndDecryptIsRunning) return; // If there is already a decrypt process, we will exit instead of waiting (because we don't need 2 subsequent decryptions)
  987.                 formatAndDecryptIsRunning = true;
  988.                 changeStatusLabel(); // Add "Decrypting..." message
  989.                 try {
  990.                         var message = document.getElementById("chatmessages").innerHTML;
  991.  
  992.                         // TODO: man darf kein ")" im benutzernamen haben!!!!
  993.  
  994.                         // first decode
  995.                         message = message.replace(/\(dec_(.*?)_(.+?)_ver(.+?): (.+?)\)/g, cbDecrypt);
  996.  
  997.                         // then do formating
  998.                         message = message.replace(/\(date: (.+?)\)/g, cbDate);
  999.                         message = message.replace(/\(user: (.+?)\)/g, cbUsername);
  1000.  
  1001.                         // make links clickable
  1002.                         message = replaceURLWithHTMLLinks(message);
  1003.  
  1004.                         // Only refresh if something has been changed. otherwise we could not copy-paste when it is permanently refreshing
  1005.                         if (document.getElementById("chatmessages").innerHTML != message) {
  1006.                                 document.getElementById("chatmessages").innerHTML = message;
  1007.                         }
  1008.                 } finally {
  1009.                         // Release the mutex and remove the "Decrypting..." message
  1010.                         formatAndDecryptIsRunning = false;
  1011.                         changeStatusLabel(); // Remove "Decrypting..." message
  1012.                 }
  1013.         }
  1014.  
  1015.         // --- Status label function
  1016.  
  1017.         function alertUser() {
  1018.                 // TODO: play sound?
  1019.         }
  1020.  
  1021.         function changeStatusLabel() {
  1022.                 var status = "";
  1023.  
  1024.                 if (addMessagePendingQueue.length > 0) {
  1025.                         status = "Sending (" + addMessagePendingQueue.length + ")...";
  1026.                 } else if (formatAndDecryptIsRunning) {
  1027.                         status = "Decrypting...";
  1028.                 } else if (refreshFailedTimerSet) {
  1029.                         status = "Checking...";
  1030.                 } else {
  1031.                         status = curseq + " posts";
  1032.                 }
  1033.  
  1034.                 document.getElementById("status").innerHTML = status;
  1035.  
  1036.                 var title = ""; // document.title;
  1037.  
  1038.                 // If tab is not in focus, we might want to alert the user when new posts arrived
  1039.                 if ((curseq > 0) && (!curVisible) && (backgroundSeqCount != curseq)) {
  1040.                         blink = 1 - blink;
  1041.                         title = "(" + (curseq-backgroundSeqCount) + ") " + htmlentities(room);
  1042.                         if (blink == 1) {
  1043.                                 title = title.toUpperCase();
  1044.                         } else {
  1045.                                 title = title.toLowerCase();
  1046.                         }
  1047.                 } else {
  1048.                         title = htmlentities(room);
  1049.                 }
  1050.                 title += " - " + htmlentities(product);
  1051.  
  1052.                 if (document.title != title) document.title = title;
  1053.         }
  1054.  
  1055.         // --- Refresh
  1056.  
  1057.         var refreshTimer = null;
  1058.         var refreshTimerSet = false;
  1059.         var refreshFailedTimer = null;
  1060.         var refreshFailedTimerSet = false;
  1061.  
  1062.         function refresh_failed(id) {
  1063.                 sajax_cancel(id);
  1064.  
  1065.                 // var refreshFailedTimerSet = (typeof refreshFailedTimer !== 'undefined');
  1066.                 // var refreshFailedTimerSet = (refreshFailedTimer !== null);
  1067.                 if (refreshFailedTimerSet) {
  1068.                         clearTimeout(refreshFailedTimer);
  1069.                         refreshFailedTimer = null;
  1070.                         refreshFailedTimerSet = false;
  1071.                 }
  1072.  
  1073.                 // Remove "Checking..." message
  1074.                 // changeStatusLabel();
  1075.  
  1076.                 // TODO: die meldung kommt viel zu oft, auch nachdem alles wieder geht. warum?
  1077. //              alert("Refresh failed. Will try again.");
  1078.                 refresh();
  1079.         }
  1080.  
  1081.         function refresh_cb(data) {
  1082.                 // var refreshFailedTimerSet = (typeof refreshFailedTimer !== 'undefined');
  1083.                 // var refreshFailedTimerSet = (refreshFailedTimer !== null);
  1084.                 if (refreshFailedTimerSet) {
  1085.                         clearTimeout(refreshFailedTimer);
  1086.                         refreshFailedTimer = null;
  1087.                         refreshFailedTimerSet = false;
  1088.                 }
  1089.  
  1090.                 var newcurseq = data[0];
  1091.                 var new_data  = data[1];
  1092.                 var userstats = data[2];
  1093.  
  1094.                 // User list
  1095.                 document.getElementById("activeusers").innerHTML = '<b>ACTIVE USERS:</b><br><br>';
  1096.                 for (var i = 0; i < userstats.length; ++i ) {
  1097.                         var username = userstats[i]['USERNAME'];
  1098.                         var color    = colorizeUsername(username);
  1099.                         var postfix  = (userstats[i]['UNIQUEID'] == uniqueID) ? ' (you)' : '';
  1100.                         document.getElementById("activeusers").innerHTML += '<font color="'+color+'"><b>'+htmlentities(username)+'</b></font>'+postfix+'<br>';
  1101.                 }
  1102.  
  1103.                 var timeout = <?php echo TIMER_1; ?>; // default timeout
  1104.                 if (newcurseq != curseq) {
  1105.                         curseq = newcurseq;
  1106.  
  1107.                         document.getElementById("chatmessages").innerHTML += new_data;
  1108.  
  1109.                         formatAndDecrypt();
  1110.  
  1111.                         if (autoscrollEnabled()) {
  1112.                                 scrollToBottom();
  1113.                         }
  1114.  
  1115.                         if (!curVisible) {
  1116.                                 // blink = 0;
  1117.                                 alertUser();
  1118.                         }
  1119.  
  1120.                         timeout = <?php echo TIMER_2; ?>; // shorter timeout when a new message has just arrived
  1121.                 }
  1122.  
  1123.                 // Remove "Checking..." message
  1124.                 changeStatusLabel();
  1125.  
  1126.                 if (!refreshTimerSet) {
  1127.                         refreshTimer = setTimeout("refresh()", timeout);
  1128.                         refreshTimerSet = true;
  1129.                 }
  1130.         }
  1131.  
  1132.         function refresh() {
  1133.                 // Only run the timer once (since we also call refresh() when a new post is added)
  1134.                 // var refreshTimerSet = (typeof refreshTimer !== 'undefined');
  1135.                 // var refreshTimerSet = (refreshTimer !== null);
  1136.                 if (refreshTimerSet) {
  1137.                         clearTimeout(refreshTimer);
  1138.                         refreshTimer = null;
  1139.                         refreshTimerSet = false;
  1140.                 }
  1141.  
  1142.                 var sajax_id = x_refresh(room, curseq, user, uniqueID, refresh_cb);
  1143.  
  1144.                 // TODO: gibt es eine bessere möglichkeit festzustellen, ob der request gestorben ist? ein negativer callback z.B.?
  1145.                 if (!refreshFailedTimerSet) {
  1146.                         refreshFailedTimer = setTimeout(function() { refresh_failed(sajax_id); }, <?php echo TIMER_DEAD; ?>);
  1147.                         refreshFailedTimerSet = true;
  1148.                 }
  1149.  
  1150.                 // Add "Checking..." message
  1151.                 changeStatusLabel();
  1152.         }
  1153.  
  1154.         // --- Add
  1155.  
  1156.         var addMessagePendingQueue = [];
  1157.  
  1158.         function do_encrypt(msg) {      // NG: "Encrypting" status message
  1159.                 var msgToServer = "";
  1160.  
  1161.                 var lines = msg.split("\n");
  1162.                 for (var i = 0; i < lines.length; ++i ) {
  1163.                         var line = lines[i];
  1164.  
  1165.                         // encrypt message
  1166.                         if (encryptionEnabled()) {
  1167.                                 var version   = <?php echo CFG_CIPHERSUITE; ?>;
  1168.                                 var passwd    = getPassword();
  1169.                                 var entrySalt = randomString(20);
  1170.                                 var hash      = specialHash(passwd, entrySalt, version);
  1171.                                 line = "(dec_" + hash + ": " + aes_enc(line, version) + ")";
  1172.                         }
  1173.  
  1174.                         msgToServer += line + "\n";
  1175.                 }
  1176.  
  1177.                 return msgToServer.trim();
  1178.         }
  1179.  
  1180.         function add_get_index_of(token) {
  1181.                 for (var i = 0; i < addMessagePendingQueue.length; ++i ) {
  1182.                         if (addMessagePendingQueue[i].token == token) return i;
  1183.                 }
  1184.                 return -1;
  1185.         }
  1186.  
  1187.         function add_failed(token) {
  1188.                 var i = add_get_index_of(token);
  1189.                 if (i == -1) return;
  1190.  
  1191.                 sajax_cancel(addMessagePendingQueue[i].sajax_id);
  1192.  
  1193.                 var failedMsg = addMessagePendingQueue[i].message;
  1194.                 addMessagePendingQueue.remove(i);
  1195.                 changeStatusLabel(); // Remove "Sending..." message
  1196.  
  1197.                 // Nachricht ausgeben.
  1198.                 // QUE TODO: automatisch neu versuchen?
  1199.                 // TODO: reihenfolge der nachrichten stimmt nicht.
  1200.                 alert("Following message could not be sent. Please try again:" + failedMsg);
  1201.  
  1202.                 // Add message back to the input box, so the user can send it again
  1203.                 if (document.getElementById("line").value.trim() == initMsg.trim()) {
  1204.                         // If login message fails, then clear box first before append
  1205.                         document.getElementById("line").value = "";
  1206.                 }
  1207.                 var newMsgCont = document.getElementById("line").value.trim() + "\n" + failedMsg.trim();
  1208.                 document.getElementById("line").value = newMsgCont.trim();
  1209.         }
  1210.  
  1211.         function add_cb(data) {
  1212.                 var token = data[0];
  1213.  
  1214.                 var i = add_get_index_of(token);
  1215.                 if (i == -1) return;
  1216.  
  1217.                 clearTimeout(addMessagePendingQueue[i].failTimer);
  1218.                 addMessagePendingQueue.remove(i);
  1219.                 changeStatusLabel(); // Remove "Sending..." message
  1220.  
  1221.                 // Refresh now, so that the new post quickly appears!
  1222.                 refresh();
  1223.         }
  1224.  
  1225.         var addIsRunning = false; // to prevent that two instances of add() run simultaneously
  1226.         function add() {
  1227.                 // Set the mutex
  1228.                 while (addIsRunning);
  1229.                 addIsRunning = true;
  1230.                 try {
  1231.                         var msg = document.getElementById("line").value.trim();
  1232.                         if ((msg == "") || (msg == initMsg.trim())) {
  1233.                                 return;
  1234.                         } else {
  1235.                                 document.getElementById("line").value = "";
  1236.                         }
  1237.  
  1238.                         send_msg(msg, 1);
  1239.  
  1240.                         sleep(100); // damit es zu keiner racecondition (= posts in falscher reihenfolge) kommen kann
  1241.                 } finally {
  1242.                         // Release the mutex
  1243.                         addIsRunning = false;
  1244.                 }
  1245.         }
  1246.  
  1247.         function send_msg(msg, encrypt) {
  1248.                 var msgToServer = msg;
  1249.  
  1250.                 if (encrypt) {
  1251.                         msgToServer = do_encrypt(msgToServer);
  1252.                 }
  1253.  
  1254.                 // We need a token which is sent back by the server, so our succeed-callback can find and remove the entry from the queue.
  1255.                 // We cannot use the sajax_id generated by SAJAX, because we need to send it to the server.
  1256.                 var token = getToken();
  1257.                 var sajax_id = x_add_line(token, room, user, msgToServer, add_cb);
  1258.  
  1259.                 // Backup the text, so we can back it up if it fails
  1260.                 // TODO: gibt es eine bessere möglichkeit festzustellen, ob der request gestorben ist? ein negativer callback z.B.?
  1261.                 var t = setTimeout(function() { add_failed(token); }, <?php echo TIMER_DEAD; ?>);
  1262.                 addMessagePendingQueue.push({
  1263.                         "token": token,
  1264.                         "sajax_id": sajax_id,
  1265.                         "message": msg,
  1266.                         "failTimer": t
  1267.                 } );
  1268.                 changeStatusLabel(); // Add "Sending..." message
  1269.  
  1270.                 return sajax_id;
  1271.         }
  1272.  
  1273.         // --- Logoff
  1274.  
  1275.         var logoffEventFired = 0;
  1276.         function send_logoff() {
  1277.                 if (logoffEventFired == 1) return;
  1278.                 logoffEventFired = 1;
  1279.  
  1280.                 send_msg("(Logging off)", 0);
  1281.  
  1282.                 document.getElementById("pwd").value = "";
  1283.  
  1284.                 // TODO: go back to login form
  1285.         }
  1286.  
  1287.         var logonEventFired = 0;
  1288.         function send_logon() {
  1289.                 if (logonEventFired == 1) return;
  1290.                 logonEventFired = 1;
  1291.  
  1292.                 send_msg("(Logging in)", 0);
  1293.         }
  1294.  
  1295.         // --- Initialization stuff
  1296.  
  1297.         var initMsg = "(enter your message here)"; /* const */
  1298.  
  1299.         function initShowHideHandlers() {
  1300.                 // https://stackoverflow.com/questions/1060008/is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active
  1301.  
  1302.                 var hidden = "hidden";
  1303.  
  1304.                 // Standards:
  1305.                 if (hidden in document)
  1306.                         document.addEventListener("visibilitychange", onchange);
  1307.                 else if ((hidden = "mozHidden") in document)
  1308.                         document.addEventListener("mozvisibilitychange", onchange);
  1309.                 else if ((hidden = "webkitHidden") in document)
  1310.                         document.addEventListener("webkitvisibilitychange", onchange);
  1311.                 else if ((hidden = "msHidden") in document)
  1312.                         document.addEventListener("msvisibilitychange", onchange);
  1313.                 // IE 9 and lower:
  1314.                 else if ('onfocusin' in document)
  1315.                         document.onfocusin = document.onfocusout = onchange;
  1316.                 // All others:
  1317.                 else
  1318.                         window.onpageshow = window.onpagehide = window.onfocus = window.onblur = onchange;
  1319.  
  1320.                 function onchange(evt) {
  1321.                         var v = true, h = false,
  1322.                         evtMap = {
  1323.                                 focus:v, focusin:v, pageshow:v, blur:h, focusout:h, pagehide:h
  1324.                         };
  1325.  
  1326.                         evt = evt || window.event;
  1327.                         var res;
  1328.                         if (evt.type in evtMap) {
  1329.                                 res = evtMap[evt.type];
  1330.                         } else {
  1331.                                 res = !this[hidden];
  1332.                         }
  1333.  
  1334.                         if (res) {
  1335.                                 onChatShow();
  1336.                         } else {
  1337.                                 onChatHide();
  1338.                         }
  1339.                 }
  1340.         }
  1341.  
  1342.         function initReturnKeyHandler() {
  1343.                 var wage = document.getElementById("line");
  1344.                 wage.addEventListener("keydown", function (e) {
  1345.                         if (e.which == 13 || e.keyCode === 13) {  //checks whether the pressed key is "Enter"
  1346.                                 add();
  1347.                         }
  1348.                 });
  1349.                 // important for <textarea> otherwise we'll have a #13 in the box after it
  1350.                 wage.addEventListener("keyup", function (e) {
  1351.                         if (e.which == 13 || e.keyCode === 13) {  //checks whether the pressed key is "Enter"
  1352.                                 document.getElementById("line").value = "";
  1353.                         }
  1354.                 });
  1355.         }
  1356.  
  1357.         function initUnloadHandler() {
  1358.                 // TODO: does not work when following a link or when clicking "back button"
  1359.                 window.onbeforeunload = function() {
  1360.                         send_logoff();
  1361.                 }
  1362.                 window.addEventListener("beforeunload", function(e) {
  1363.                         send_logoff();
  1364.                 }, false);
  1365.         }
  1366.  
  1367.         // Delayed (single run) onkeydown event for the password field
  1368.         var delayedKeyChangeTimer = null;
  1369.         var delayedKeyChangeTimerSet = false;
  1370.         function initKeyChangeHandler() {
  1371.                 // We use onkeydown instead of onkeypress, otherwise "del" or "ctrl+v" won't work on Chrome
  1372.                 // See http://help.dottoro.com/ljlwfxum.php
  1373.  
  1374.                 document.getElementById("pwd").onkeydown = function(evt) {
  1375.                         var enterPressed = (evt.which == 13 || evt.keyCode === 13);
  1376.  
  1377.                         // var delayedKeyChangeTimerSet = (typeof delayedKeyChangeTimer !== 'undefined');
  1378.                         // var delayedKeyChangeTimerSet = (delayedKeyChangeTimer !== null);
  1379.                         if (delayedKeyChangeTimerSet) {
  1380.                                 clearTimeout(delayedKeyChangeTimer);
  1381.                                 delayedKeyChangeTimer = null;
  1382.                                 delayedKeyChangeTimerSet = false;
  1383.                         }
  1384.  
  1385.                         var timeout = enterPressed ? 0 : <?php echo TIMER_KEYCHANGE; ?>;
  1386.                         if (!delayedKeyChangeTimerSet) {
  1387.                                 delayedKeyChangeTimer = setTimeout("formatAndDecrypt()", <?php echo TIMER_KEYCHANGE; ?>);
  1388.                                 delayedKeyChangeTimerSet = true;
  1389.                         }
  1390.                 };
  1391.         }
  1392.  
  1393.         function initPage() {
  1394.                 uniqueID=randomString(20);
  1395.                 // document.getElementById("pwd").value  = "";
  1396.                 document.getElementById("line").value = initMsg;
  1397.                 changeStatusLabel();
  1398.                 initUnloadHandler();
  1399.                 initReturnKeyHandler();
  1400.                 initShowHideHandlers();
  1401.                 initKeyChangeHandler();
  1402.                 refresh();
  1403.                 // We do it at last, otherwise we will have the problem that the chat will not be scrolled to the very bottom in the beginning
  1404.                 send_logon();
  1405.         }
  1406.  
  1407.         // --- Misc event handlers
  1408.  
  1409.         function onChatShow() {
  1410.                 curVisible = true;
  1411.                 changeStatusLabel();
  1412.         }
  1413.  
  1414.         function onChatHide() {
  1415.                 curVisible = false;
  1416.                 blink = 1;
  1417.                 backgroundSeqCount = curseq;
  1418.                 // changeStatusLabel();
  1419.         }
  1420.  
  1421.         function onFocusChatField() {
  1422.                 if (document.getElementById("line").value == initMsg) {
  1423.                         document.getElementById("line").value = "";
  1424.                 }
  1425.         }
  1426.  
  1427.         </script>
  1428.  
  1429. </head>
  1430.  
  1431. <body class="chatroom" onload="initPage();">
  1432.  
  1433. <div id="header">
  1434.         <font size="+2"><font color="orange"><?php echo htmlentities(PRODUCT_NAME); ?> <font size="-1"><?php
  1435.                 echo htmlentities(MCC_VER);
  1436.         ?></font></font> <span class="userid">[<?php
  1437.                 echo htmlentities("$ses_user@$ses_room");
  1438.         ?>]</span></font><br>
  1439.         <span class="pwdenter">Password for client-side encryption of messages: <input type="password" name="pwd" id="pwd" value=""></span>
  1440.         <span class="logout"><a href="<?php echo $_SERVER['SCRIPT_NAME']; ?>" onClick="send_logoff();return true;">Back to login form</a></span>
  1441. </div>
  1442. <div id="status"><em>Loading...</em></div>
  1443. <div id="activeusers"></div>
  1444. <div id="chatmessages"></div>
  1445. <form name="f" action="#" onsubmit="add();return false;">
  1446.         <div id="postarea">
  1447.                 <textarea cols="70" rows="3" name="line" id="line" value="" onfocus="onFocusChatField()"></textarea>
  1448.                 <input type="button" name="check" value="Post message" onclick="add();return false;" class="button">
  1449.                 <table width="100%" border="0" cellspacing="0" cellpadding="0">
  1450.                         <tr>
  1451.                                 <td align="left" width="50%"><label><input type="checkbox" id="autoscroll" name="autoscroll" value="yes" checked> Scroll down when new message arrives</label><br></td>
  1452.                                 <td align="left" width="50%"><label><input type="checkbox" id="doencrypt"  name="doencrypt"  value="yes" checked> Encrypt all messages</label><br></td>
  1453.                         </tr>
  1454.                 </table>
  1455.         </div>
  1456. </form>
  1457.  
  1458. </body>
  1459.  
  1460. <?php
  1461.  
  1462. } else if ($show_list) {
  1463.  
  1464. ?>
  1465.  
  1466. </head>
  1467.  
  1468. <body>
  1469.  
  1470. <h1><?php echo htmlentities(PRODUCT_NAME); ?> <?php echo htmlentities(MCC_VER); ?></h1>
  1471.  
  1472. <h2>List chat rooms</h2>
  1473.  
  1474. <table border="1" cellpadding="4" cellspacing="0">
  1475. <thead><tr>
  1476.         <td>Chat room</td>
  1477.         <td>Messages</td>
  1478.         <td>Users</td>
  1479.         <td>Last activity</td>
  1480. </tr></thead>
  1481. <tbody>
  1482.         <?php
  1483.  
  1484.         $rooms = list_rooms();
  1485.         foreach ($rooms as &$room) {
  1486.                 $room_file = chatroom_file($room);
  1487.                 $messages = count_messages($room);
  1488.                 $users = count_users($room);
  1489.                 $last_activity = date('Y-m-d H:i:s o', filemtime($room_file));
  1490.                 echo '<tr>';
  1491.                 echo '<td>'.htmlentities($room).'</td>';
  1492.                 echo '<td>'.htmlentities($messages).'</td>';
  1493.                 echo '<td>'.htmlentities($users).'</td>';
  1494.                 echo '<td>'.htmlentities($last_activity).'</td>';
  1495.                 echo '</tr>';
  1496.         }
  1497.  
  1498.         ?>
  1499. </tbody>
  1500. </table>
  1501.  
  1502. <p><a href="<?php echo $_SERVER['SCRIPT_NAME']; ?>">Back to login</a></p>
  1503.  
  1504. <?php
  1505.  
  1506. } else {
  1507.  
  1508. ?>
  1509.  
  1510. </head>
  1511.  
  1512. <body>
  1513.  
  1514. <h1><?php echo htmlentities(PRODUCT_NAME); ?> <?php echo htmlentities(MCC_VER); ?></h1>
  1515.  
  1516. <h2>Login</h2>
  1517.  
  1518. <form action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="POST">
  1519. <input type="hidden" name="sent" value="1">
  1520.  
  1521. <?php
  1522.  
  1523. if (isset($_POST['sent'])) {
  1524.         if ($ses_room == '') {
  1525.                 echo "<p><span class=\"error\"><b>Error:</b> You must enter a chat room name.</span></p>";
  1526.         } elseif (!$chat_exists) {
  1527.                 echo "<p><span class=\"error\"><b>Error:</b> Chat room <b>".htmlentities($ses_room)."</b> does not exist. Please ask an administrator to create one.</span></p>";
  1528.         }
  1529. }
  1530.  
  1531. ?>
  1532.  
  1533. Room<?php if (CHAT_MUST_EXIST) echo " (chat room must exist)"; ?>:<br>
  1534. <input type="text" name="ses_room" value="<?php echo $ses_room; ?>"><br><br>
  1535.  
  1536. <?php
  1537.  
  1538. if (isset($_POST['sent'])) {
  1539.         if ($ses_user == '') {
  1540.                 echo "<p><span class=\"error\"><b>Error:</b> You must enter an username.</span></p>";
  1541.         }
  1542. }
  1543.  
  1544. ?>
  1545.  
  1546. Username (you can freely choose it yourself):<br>
  1547. <input type="text" name="ses_user" value="<?php echo $ses_user; ?>"><br><br>
  1548.  
  1549. <input type="submit" value="Login" class="button">
  1550.  
  1551. <?php
  1552. if (CHAT_MUST_EXIST) {
  1553.         if (isset($_POST['sent'])) {
  1554.                 if (($admin_pwd != '') && (!adminAuth($admin_pwd, ACTION_CREATE_ROOM))) {
  1555.                         echo "<p><span class=\"error\"><b>Error:</b> This is not the correct administrator password.</span></p>";
  1556.                 }
  1557.         }
  1558.         ?>
  1559.         <br><br><br>Optional: Admin-Password for creating a new chat:<br>
  1560.         <input type="password" name="admin_pwd" value=""><br><br>
  1561.         <?php
  1562. }
  1563. ?>
  1564.  
  1565. </form>
  1566.  
  1567. <hr>
  1568.  
  1569. <h2>List chat rooms</h2>
  1570.  
  1571. <form action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="POST">
  1572. <input type="hidden" name="list_chatrooms" value="1">
  1573.  
  1574. <?php
  1575. if (LIST_CHATS_REQUIRE_PWD) {
  1576.         if (isset($_POST['list_chatrooms'])) {
  1577.                 echo "<p><span class=\"error\"><b>Error:</b> Wrong password.</span></p>";
  1578.         }
  1579.         ?>
  1580.         Admin-Password for listing chats:<br>
  1581.         <input type="password" name="admin_pwd" value=""><br><br>
  1582.         <?php
  1583. }
  1584. ?>
  1585.  
  1586. <input type="submit" value="List chat rooms" class="button"><br><br>
  1587.  
  1588. </form>
  1589.  
  1590. <hr>
  1591.  
  1592. <p><a href="http://www.viathinksoft.com/redir.php?id=324897" target="_blank"><?php echo htmlentities(PRODUCT_NAME); ?> <?php echo htmlentities(MCC_VER); ?></a> &copy; 2014-2018 <a href="http://www.viathinksoft.com/" target="_blank">ViaThinkSoft</a>.</a></p>
  1593.  
  1594. </body>
  1595.  
  1596. <?php
  1597. }
  1598. ?>
  1599.  
  1600. </html>
  1601.