Subversion Repositories cryptochat

Rev

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

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