Subversion Repositories cryptochat

Rev

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