Subversion Repositories cryptochat

Rev

Rev 6 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 daniel-mar 1
<?php
2
 
5 daniel-mar 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
 */
2 daniel-mar 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
 
7 daniel-mar 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
 
2 daniel-mar 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);
6 daniel-mar 231
        $users_lookup = array();
2 daniel-mar 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';
6 daniel-mar 296
sajax_export( /** @phpstan-ignore-line */ // PHPstan thinks that sajax_export() only accepts 0 parameters?!
2 daniel-mar 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>