Subversion Repositories cryptochat

Rev

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