Passed
Pull Request — master (#7021)
by
unknown
09:02
created

Chat::startSession()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 17
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
use Chamilo\CoreBundle\Entity\UserRelUser;
5
use ChamiloSession as Session;
6
7
/**
8
 * Class Chat.
9
 *
10
 * @todo ChamiloSession instead of $_SESSION
11
 */
12
class Chat extends Model
13
{
14
    public $columns = [
15
        'id',
16
        'from_user',
17
        'to_user',
18
        'message',
19
        'sent',
20
        'recd',
21
    ];
22
    public $window_list = [];
23
24
    /**
25
     * The contructor sets the chat table name and the window_list attribute.
26
     */
27
    public function __construct()
28
    {
29
        parent::__construct();
30
        $this->table = Database::get_main_table(TABLE_MAIN_CHAT);
31
        $this->window_list = Session::read('window_list');
32
        Session::write('window_list', $this->window_list);
33
    }
34
35
    /**
36
     * Get user chat status.
37
     *
38
     * @return int 0 if disconnected, 1 if connected
39
     */
40
    public function getUserStatus()
41
    {
42
        $status = UserManager::get_extra_user_data_by_field(
43
            api_get_user_id(),
44
            'user_chat_status',
45
            false,
46
            true
47
        );
48
49
        return $status['user_chat_status'];
50
    }
51
52
    /**
53
     * Set user chat status.
54
     *
55
     * @param int $status 0 if disconnected, 1 if connected
56
     */
57
    public function setUserStatus($status)
58
    {
59
        UserManager::update_extra_field_value(
60
            api_get_user_id(),
61
            'user_chat_status',
62
            $status
63
        );
64
    }
65
66
    /**
67
     * @param int  $currentUserId
68
     * @param int  $userId
69
     * @param bool $latestMessages
70
     *
71
     * @return array
72
     */
73
    public function getLatestChat($currentUserId, $userId, $latestMessages)
74
    {
75
        $items = $this->getPreviousMessages(
76
            $currentUserId,
77
            $userId,
78
            0,
79
            $latestMessages
80
        );
81
82
        return array_reverse($items);
83
    }
84
85
    /**
86
     * @return string
87
     */
88
    public function getContacts(): string
89
    {
90
        return (string) SocialManager::listMyFriendsBlock(api_get_user_id(), '', true);
91
    }
92
93
    /**
94
     * @param array $chatHistory
95
     * @param int   $latestMessages
96
     *
97
     * @return mixed
98
     */
99
    public function getAllLatestChats($chatHistory, $latestMessages = 5)
100
    {
101
        $currentUserId = api_get_user_id();
102
103
        if (empty($chatHistory)) {
104
            return [];
105
        }
106
107
        $chats = [];
108
        foreach ($chatHistory as $userId => $time) {
109
            $total = $this->getCountMessagesExchangeBetweenUsers($userId, $currentUserId);
110
            $start = $total - $latestMessages;
111
            if ($start < 0) {
112
                $start = 0;
113
            }
114
            $items = $this->getMessages($userId, $currentUserId, $start, $latestMessages);
115
            $chats[$userId]['items'] = $items;
116
            $chats[$userId]['window_user_info'] = api_get_user_info($userId);
117
        }
118
119
        return $chats;
120
    }
121
122
    /**
123
     * Starts a chat session and returns JSON array of status and chat history.
124
     *
125
     * @return bool (prints output in JSON format)
126
     */
127
    public function startSession()
128
    {
129
        // ofaj
130
        // $chat = new Chat();
131
        // $chat->setUserStatus(1);
132
133
        $chatList = Session::read('openChatBoxes');
134
        $chats = $this->getAllLatestChats($chatList);
135
        $return = [
136
            'user_status' => $this->getUserStatus(),
137
            'me' => get_lang('Me'),
138
            'user_id' => api_get_user_id(),
139
            'items' => $chats,
140
        ];
141
        echo json_encode($return);
142
143
        return true;
144
    }
145
146
    /**
147
     * @param int $fromUserId
148
     * @param int $toUserId
149
     *
150
     * @return int
151
     */
152
    public function getCountMessagesExchangeBetweenUsers($fromUserId, $toUserId)
153
    {
154
        $row = Database::select(
155
            'count(*) as count',
156
            $this->table,
157
            [
158
                'where' => [
159
                    '(from_user = ? AND to_user = ?) OR (from_user = ? AND to_user = ?) ' => [
160
                        $fromUserId,
161
                        $toUserId,
162
                        $toUserId,
163
                        $fromUserId,
164
                    ],
165
                ],
166
            ],
167
            'first'
168
        );
169
170
        return (int) $row['count'];
171
    }
172
173
    /**
174
     * @param int $fromUserId
175
     * @param int $toUserId
176
     * @param int $visibleMessages
177
     * @param int $previousMessageCount messages to show
178
     *
179
     * @return array
180
     */
181
    public function getPreviousMessages(
182
        $fromUserId,
183
        $toUserId,
184
        $visibleMessages = 1,
185
        $previousMessageCount = 5,
186
        $orderBy = ''
187
    ) {
188
        $toUserId = (int) $toUserId;
189
        $fromUserId = (int) $fromUserId;
190
        $visibleMessages = (int) $visibleMessages;
191
        $previousMessageCount = (int) $previousMessageCount;
192
193
        $total = $this->getCountMessagesExchangeBetweenUsers($fromUserId, $toUserId);
194
        $show = $total - $visibleMessages;
195
196
        if ($show < $previousMessageCount) {
197
            $show = $previousMessageCount;
198
        }
199
        $from = $show - $previousMessageCount;
200
201
        if ($from < 0) {
202
            return [];
203
        }
204
205
        return $this->getMessages($fromUserId, $toUserId, $from, $previousMessageCount, $orderBy);
206
    }
207
208
    /**
209
     * @param int    $fromUserId
210
     * @param int    $toUserId
211
     * @param int    $start
212
     * @param int    $end
213
     * @param string $orderBy
214
     *
215
     * @return array
216
     */
217
    public function getMessages($fromUserId, $toUserId, $start, $end, $orderBy = '')
218
    {
219
        $toUserId = (int) $toUserId;
220
        $fromUserId = (int) $fromUserId;
221
        $start = (int) $start;
222
        $end = (int) $end;
223
224
        if (empty($toUserId) || empty($fromUserId)) {
225
            return [];
226
        }
227
228
        $orderBy = Database::escape_string($orderBy);
229
        if (empty($orderBy)) {
230
            $orderBy = 'ORDER BY id ASC';
231
        }
232
233
        $sql = "SELECT * FROM ".$this->table."
234
                WHERE
235
                    (
236
                        to_user = $toUserId AND
237
                        from_user = $fromUserId
238
                    )
239
                    OR
240
                    (
241
                        from_user = $toUserId AND
242
                        to_user =  $fromUserId
243
                    )
244
                $orderBy
245
                LIMIT $start, $end
246
                ";
247
        $result = Database::query($sql);
248
        $rows = Database::store_result($result);
249
        $fromUserInfo = api_get_user_info($fromUserId, true);
250
        $toUserInfo = api_get_user_info($toUserId, true);
251
        $users = [
252
            $fromUserId => $fromUserInfo,
253
            $toUserId => $toUserInfo,
254
        ];
255
        $items = [];
256
        $rows = array_reverse($rows);
257
        foreach ($rows as $chat) {
258
            $fromUserId = $chat['from_user'];
259
            $userInfo = $users[$fromUserId];
260
            $toUserInfo = $users[$toUserId];
261
262
            $items[$chat['id']] = [
263
                'id' => $chat['id'],
264
                'message' => Security::remove_XSS($chat['message']),
265
                'date' => api_strtotime($chat['sent'], 'UTC'),
266
                'recd' => $chat['recd'],
267
                'from_user_info' => $userInfo,
268
                'to_user_info' => $toUserInfo,
269
            ];
270
            $_SESSION['openChatBoxes'][$fromUserId] = api_strtotime($chat['sent'], 'UTC');
271
        }
272
273
        return $items;
274
    }
275
276
    /**
277
     * Refreshes the chat windows (usually called every x seconds through AJAX).
278
     */
279
    public function heartbeat()
280
    {
281
        $chatHistory = Session::read('chatHistory');
282
        $currentUserId = api_get_user_id();
283
284
        // update current chats
285
        if (!empty($chatHistory) && is_array($chatHistory)) {
286
            foreach ($chatHistory as $fromUserId => &$data) {
287
                $userInfo = api_get_user_info($fromUserId, true);
288
                $count = $this->getCountMessagesExchangeBetweenUsers($fromUserId, $currentUserId);
289
                $chatItems = $this->getLatestChat($fromUserId, $currentUserId, 5);
290
                $data['window_user_info'] = $userInfo;
291
                $data['items'] = $chatItems;
292
                $data['total_messages'] = $count;
293
            }
294
        }
295
296
        $sql = "SELECT * FROM ".$this->table."
297
                WHERE
298
                    to_user = '".$currentUserId."' AND recd = 0
299
                ORDER BY id ASC";
300
        $result = Database::query($sql);
301
302
        $chatList = [];
303
        while ($chat = Database::fetch_assoc($result)) {
304
            $chatList[$chat['from_user']][] = $chat;
305
        }
306
307
        foreach ($chatList as $fromUserId => $messages) {
308
            $userInfo = api_get_user_info($fromUserId, true);
309
            $count = $this->getCountMessagesExchangeBetweenUsers($fromUserId, $currentUserId);
310
            $chatItems = $this->getLatestChat($fromUserId, $currentUserId, 5);
311
312
            // Cleaning tsChatBoxes
313
            unset($_SESSION['tsChatBoxes'][$fromUserId]);
314
315
            foreach ($messages as $chat) {
316
                $_SESSION['openChatBoxes'][$fromUserId] = api_strtotime($chat['sent'], 'UTC');
317
            }
318
319
            $chatHistory[$fromUserId] = [
320
                'window_user_info' => $userInfo,
321
                'total_messages' => $count,
322
                'items' => $chatItems,
323
            ];
324
        }
325
326
        Session::write('chatHistory', $chatHistory);
327
328
        $sql = "UPDATE ".$this->table."
329
                SET recd = 1
330
                WHERE to_user = $currentUserId AND recd = 0";
331
        Database::query($sql);
332
333
        echo json_encode(['items' => $chatHistory]);
334
    }
335
336
    /**
337
     * Saves into session the fact that a chat window exists with the given user.
338
     *
339
     * @param int $userId
340
     */
341
    public function saveWindow($userId)
342
    {
343
        $this->window_list[$userId] = true;
344
        Session::write('window_list', $this->window_list);
345
    }
346
347
    /**
348
     * Sends a message from one user to another user.
349
     *
350
     * @param int    $fromUserId  The ID of the user sending the message
351
     * @param int    $to_user_id  The ID of the user receiving the message
352
     * @param string $message     Message
353
     * @param bool   $printResult Optional. Whether print the result
354
     * @param bool   $sanitize    Optional. Whether sanitize the message
355
     */
356
    public function send(
357
        $fromUserId,
358
        $to_user_id,
359
        $message,
360
        $printResult = true,
361
        $sanitize = true
362
    ) {
363
        $relation = SocialManager::get_relation_between_contacts($fromUserId, $to_user_id);
364
365
        if ($relation === UserRelUser::USER_RELATION_TYPE_FRIEND
366
            || $relation === UserRelUser::USER_RELATION_TYPE_GOODFRIEND) {
367
            $now = api_get_utc_datetime();
368
            $user_info = api_get_user_info($to_user_id, true);
369
            $this->saveWindow($to_user_id);
370
            $_SESSION['openChatBoxes'][$to_user_id] = api_strtotime($now, 'UTC');
371
372
            if ($sanitize) {
373
                $messagesan = $this->sanitize($message);
374
            } else {
375
                $messagesan = $message;
376
            }
377
378
            if (!isset($_SESSION['chatHistory'][$to_user_id])) {
379
                $_SESSION['chatHistory'][$to_user_id] = [];
380
            }
381
            $item = [
382
                's' => '1',
383
                'f' => $fromUserId,
384
                'm' => $messagesan,
385
                'date' => api_strtotime($now, 'UTC'),
386
                'username' => get_lang('Me'),
387
            ];
388
            $_SESSION['chatHistory'][$to_user_id]['items'][] = $item;
389
            $_SESSION['chatHistory'][$to_user_id]['user_info']['user_name'] = $user_info['complete_name'];
390
            $_SESSION['chatHistory'][$to_user_id]['user_info']['online'] = $user_info['user_is_online'];
391
            $_SESSION['chatHistory'][$to_user_id]['user_info']['avatar'] = $user_info['avatar_small'];
392
            $_SESSION['chatHistory'][$to_user_id]['user_info']['user_id'] = $user_info['user_id'];
393
394
            unset($_SESSION['tsChatBoxes'][$to_user_id]);
395
396
            $params = [];
397
            $params['from_user'] = (int) $fromUserId;
398
            $params['to_user'] = (int) $to_user_id;
399
            $params['message'] = $messagesan;
400
            $params['sent'] = api_get_utc_datetime();
401
            $params['recd']      = 0;
402
403
            if (!empty($fromUserId) && !empty($to_user_id)) {
404
                $messageId = $this->save($params);
405
                if ($printResult) {
406
                    echo $messageId;
0 ignored issues
show
Bug introduced by
Are you sure $messageId of type false|integer can be used in echo? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

406
                    echo /** @scrutinizer ignore-type */ $messageId;
Loading history...
407
                    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
408
                }
409
            }
410
        }
411
412
        if ($printResult) {
413
            echo '0';
414
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
415
        }
416
    }
417
418
    /**
419
     * Close a specific chat box (user ID taken from $_POST['chatbox']).
420
     *
421
     * @param int $userId
422
     */
423
    public function closeWindow($userId)
424
    {
425
        if (empty($userId)) {
426
            return false;
427
        }
428
429
        $list = Session::read('openChatBoxes');
430
        if (isset($list[$userId])) {
431
            unset($list[$userId]);
432
            Session::write('openChatBoxes', $list);
433
        }
434
435
        $list = Session::read('chatHistory');
436
        if (isset($list[$userId])) {
437
            unset($list[$userId]);
438
            Session::write('chatHistory', $list);
439
        }
440
441
        return true;
442
    }
443
444
    /**
445
     * Close chat - disconnects the user.
446
     */
447
    public function close()
448
    {
449
        Session::erase('tsChatBoxes');
450
        Session::erase('openChatBoxes');
451
        Session::erase('chatHistory');
452
        Session::erase('window_list');
453
    }
454
455
    /**
456
     * Filter chat messages to avoid XSS or other JS.
457
     *
458
     * @param string $text Unfiltered message
459
     *
460
     * @return string Filtered message
461
     */
462
    public function sanitize($text)
463
    {
464
        $text = htmlspecialchars($text, ENT_QUOTES);
465
        $text = str_replace("\n\r", "\n", $text);
466
        $text = str_replace("\r\n", "\n", $text);
467
        $text = str_replace("\n", "<br>", $text);
468
469
        return $text;
470
    }
471
472
    /**
473
     * SET Disable Chat.
474
     *
475
     * @param bool $status to disable chat
476
     */
477
    public static function setDisableChat($status = true)
478
    {
479
        Session::write('disable_chat', $status);
480
    }
481
482
    /**
483
     * Disable Chat - disable the chat.
484
     *
485
     * @return bool - return true if setDisableChat status is true
486
     */
487
    public static function disableChat()
488
    {
489
        $status = Session::read('disable_chat');
490
        if (!empty($status)) {
491
            if (true == $status) {
492
                Session::write('disable_chat', null);
493
494
                return true;
495
            }
496
        }
497
498
        return false;
499
    }
500
501
    /**
502
     * @return bool
503
     */
504
    public function isChatBlockedByExercises()
505
    {
506
        $currentExercises = Session::read('current_exercises');
507
        if (!empty($currentExercises)) {
508
            foreach ($currentExercises as $attempt_status) {
509
                if (true == $attempt_status) {
510
                    return true;
511
                }
512
            }
513
        }
514
515
        return false;
516
    }
517
518
    public function ackReadUpTo(int $fromUserId, int $toUserId, int $lastSeenMessageId): int
519
    {
520
        if ($fromUserId <= 0 || $toUserId <= 0 || $lastSeenMessageId <= 0) {
521
            return 0;
522
        }
523
524
        $sql = "UPDATE {$this->table}
525
            SET recd = 2
526
            WHERE from_user = {$fromUserId}
527
              AND to_user   = {$toUserId}
528
              AND id       <= {$lastSeenMessageId}
529
              AND recd < 2";
530
531
        $res = Database::query($sql);
532
        return $res ? Database::affected_rows($res) : 0;
533
    }
534
535
    public function heartbeatMin(int $userId, int $sinceId = 0): array
536
    {
537
        $uid = (int) $userId;
538
        $sinceId = max(0, (int) $sinceId);
539
540
        $tbl = Database::get_main_table(TABLE_MAIN_CHAT);
541
        $sql = "
542
        SELECT
543
            MAX(id) AS last_id,
544
            SUM(CASE WHEN recd < 2 THEN 1 ELSE 0 END) AS unread
545
        FROM {$tbl}
546
        WHERE to_user = {$uid} AND id > {$sinceId}
547
    ";
548
        $res = Database::query($sql);
549
        $row = Database::fetch_array($res) ?: ['last_id' => 0, 'unread' => 0];
550
551
        $lastId = (int) ($row['last_id'] ?? 0);
552
        $unread = (int) ($row['unread'] ?? 0);
553
554
        return [
555
            'has_new'  => $lastId > $sinceId,
556
            'last_id'  => $lastId,
557
            'unread'   => $unread,
558
            'since_id' => $sinceId,
559
        ];
560
    }
561
562
    /**
563
     * Ultra-tiny per-peer heartbeat: returns only latest id for (peer -> me).
564
     * O(1) using composite index (to_user, from_user, id).
565
     */
566
    public function heartbeatTiny(int $userId, int $peerId, int $sinceId = 0): array
567
    {
568
        $uid  = (int) $userId;
569
        $pid  = (int) $peerId;
570
        $tbl  = Database::get_main_table(TABLE_MAIN_CHAT);
571
572
        $sql = "SELECT id
573
            FROM {$tbl}
574
            WHERE to_user = {$uid} AND from_user = {$pid}
575
            ORDER BY id DESC
576
            LIMIT 1";
577
        $res = Database::query($sql);
578
        $row = Database::fetch_assoc($res) ?: ['id' => 0];
579
        $last = (int) ($row['id'] ?? 0);
580
581
        return [
582
            'has_new'  => $last > max(0, (int) $sinceId),
583
            'last_id'  => $last,
584
            'peer_id'  => $pid,
585
            'since_id' => (int) $sinceId,
586
        ];
587
    }
588
589
    /**
590
     * Get ONLY new incoming messages (peer -> me) with id > $sinceId.
591
     * Keeps payload tiny. We do *not* fetch my own messages here.
592
     */
593
    public function getIncomingSince(int $peerId, int $meId, int $sinceId = 0): array
594
    {
595
        $tbl = Database::get_main_table(TABLE_MAIN_CHAT);
596
        $pid = (int) $peerId;
597
        $uid = (int) $meId;
598
        $sid = max(0, (int) $sinceId);
599
600
        $sql = "SELECT id, from_user, to_user, message, sent, recd
601
            FROM {$tbl}
602
            WHERE from_user = {$pid}
603
              AND to_user   = {$uid}
604
              AND id       > {$sid}
605
            ORDER BY id ASC
606
            LIMIT 200";
607
        $res = Database::query($sql);
608
        $rows = Database::store_result($res);
609
610
        if (empty($rows)) return [];
611
612
        $fromUserInfo = api_get_user_info($pid, true);
613
        $toUserInfo   = api_get_user_info($uid, true);
614
615
        $items = [];
616
        foreach ($rows as $chat) {
617
            $items[] = [
618
                'id'             => (int) $chat['id'],
619
                'message'        => Security::remove_XSS($chat['message']),
620
                'date'           => api_strtotime($chat['sent'], 'UTC'),
621
                'recd'           => (int) $chat['recd'],
622
                'from_user_info' => $fromUserInfo,
623
                'to_user_info'   => $toUserInfo,
624
            ];
625
        }
626
627
        // Mark as delivered
628
        $ids = array_column($rows, 'id');
629
        if (!empty($ids)) {
630
            $idsCsv = implode(',', array_map('intval', $ids));
631
            Database::query("UPDATE {$tbl} SET recd = GREATEST(recd,1) WHERE id IN ({$idsCsv})");
632
        }
633
634
        return $items;
635
    }
636
}
637