Subversion Repositories cryptochat

Rev

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

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