Subversion Repositories cryptochat

Rev

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