Passed
Push — master ( 2f402d...baa39b )
by
unknown
17:05 queued 08:04
created

ChatController::globalContacts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 12
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Traits\ControllerTrait;
10
use Chamilo\CoreBundle\Traits\CourseControllerTrait;
11
use Chamilo\CoreBundle\Traits\ResourceControllerTrait;
12
use Chamilo\CourseBundle\Controller\CourseControllerInterface;
13
use Chamilo\CourseBundle\Entity\CChatConversation;
14
use Chamilo\CourseBundle\Entity\CDocument;
15
use Chamilo\CourseBundle\Repository\CChatConversationRepository;
16
use Chamilo\CourseBundle\Repository\CDocumentRepository;
17
use Chat;
18
use CourseChatUtils;
19
use Doctrine\Persistence\ManagerRegistry;
20
use Event;
21
use Symfony\Component\HttpFoundation\JsonResponse;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpFoundation\Response;
24
use Symfony\Component\Routing\Attribute\Route;
25
use Throwable;
26
27
use const JSON_UNESCAPED_SLASHES;
28
use const JSON_UNESCAPED_UNICODE;
29
30
class ChatController extends AbstractResourceController implements CourseControllerInterface
31
{
32
    use ControllerTrait;
33
    use CourseControllerTrait;
34
    use ResourceControllerTrait;
35
36
    #[Route(path: '/resources/chat/', name: 'chamilo_core_chat_home', options: ['expose' => true])]
37
    public function index(Request $request, ManagerRegistry $doctrine): Response
38
    {
39
        Event::event_access_tool(TOOL_CHAT);
0 ignored issues
show
Bug introduced by
The method event_access_tool() does not exist on Event. ( Ignorable by Annotation )

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

39
        Event::/** @scrutinizer ignore-call */ 
40
               event_access_tool(TOOL_CHAT);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
40
        Event::registerLog([
0 ignored issues
show
Bug introduced by
The method registerLog() does not exist on Event. ( Ignorable by Annotation )

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

40
        Event::/** @scrutinizer ignore-call */ 
41
               registerLog([

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
41
            'tool' => TOOL_CHAT,
42
            'action' => 'start',
43
            'action_details' => 'start-chat',
44
        ]);
45
46
        $course = api_get_course_entity();
47
        $session = api_get_session_entity() ?: null;
48
49
        /** @var CDocumentRepository $docsRepo */
50
        $docsRepo = $doctrine->getRepository(CDocument::class);
51
        $docsRepo->ensureChatSystemFolder($course, $session);
52
53
        return $this->render('@ChamiloCore/Chat/chat.html.twig', [
54
            'restrict_to_coach' => ('true' === api_get_setting('chat.course_chat_restrict_to_coach')),
55
            'user' => api_get_user_info(),
56
            'emoji_smile' => '<span>&#128522;</span>',
57
            'course_url_params' => api_get_cidreq(),
58
            'course' => $course,
59
            'session_id' => api_get_session_id(),
60
            'group_id' => api_get_group_id(),
61
            'chat_parent_node_id' => $course->getResourceNode()->getId(),
62
        ]);
63
    }
64
65
    #[Route(path: '/resources/chat/conversations/', name: 'chamilo_core_chat_ajax', options: ['expose' => true])]
66
    public function ajax(Request $request, ManagerRegistry $doctrine): Response
67
    {
68
        $debug = false;
69
        $log = function (string $msg, array $ctx = []) use ($debug): void {
70
            if (!$debug) {
71
                return;
72
            }
73
            error_log('[ChatController] '.$msg.' | '.json_encode($ctx, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
74
        };
75
76
        if (!api_protect_course_script()) {
77
            return new JsonResponse(['status' => false, 'error' => 'forbidden'], 403);
78
        }
79
80
        $courseId = api_get_course_int_id();
81
        $userId = api_get_user_id();
82
        $sessionId = api_get_session_id();
83
        $groupId = api_get_group_id();
84
85
        $course = api_get_course_entity();
86
        $session = api_get_session_entity() ?: null;
87
88
        /** @var CChatConversationRepository $convRepo */
89
        $convRepo = $doctrine->getRepository(CChatConversation::class);
90
91
        /** @var CDocumentRepository $docsRepo */
92
        $docsRepo = $doctrine->getRepository(CDocument::class);
93
94
        $docsRepo->ensureChatSystemFolder($course, $session);
95
        $docRoot = $docsRepo->ensureChatSystemFolderUnderCourseRoot($course, $session);
96
97
        $chat = new CourseChatUtils(
98
            $courseId,
99
            $userId,
100
            $sessionId,
101
            $groupId,
102
            $docRoot,
103
            $convRepo
104
        );
105
106
        $action = (string) $request->get('action', 'track');
107
        $json = ['status' => false];
108
109
        try {
110
            switch ($action) {
111
                case 'chat_logout':
112
                    Event::registerLog([
113
                        'tool' => TOOL_CHAT,
114
                        'action' => 'exit',
115
                        'action_details' => 'exit-chat',
116
                    ]);
117
                    $json = ['status' => true];
118
119
                    break;
120
121
                case 'track':
122
                    $chat->keepUserAsConnected();
123
                    $chat->disconnectInactiveUsers();
124
125
                    $friend = (int) $request->get('friend', 0);
126
                    $newUsersOnline = $chat->countUsersOnline();
127
                    $oldUsersOnline = (int) $request->get('users_online', 0);
128
129
                    $json = [
130
                        'status' => true,
131
                        'data' => [
132
                            'oldFileSize' => false,
133
                            'history' => $chat->readMessages(false, $friend),
134
                            'usersOnline' => $newUsersOnline,
135
                            'userList' => $newUsersOnline !== $oldUsersOnline ? $chat->listUsersOnline() : null,
136
                            'currentFriend' => $friend,
137
                        ],
138
                    ];
139
140
                    break;
141
142
                case 'preview':
143
                    $msg = (string) $request->get('message', '');
144
                    $json = ['status' => true, 'data' => ['message' => CourseChatUtils::prepareMessage($msg)]];
145
146
                    break;
147
148
                case 'reset':
149
                    $friend = (int) $request->get('friend', 0);
150
                    $json = ['status' => true, 'data' => $chat->readMessages(true, $friend)];
151
152
                    break;
153
154
                case 'write':
155
                    $friend = (int) $request->get('friend', 0);
156
                    $msg = (string) $request->get('message', '');
157
                    $ok = $chat->saveMessage($msg, $friend);
158
                    $json = ['status' => $ok, 'data' => ['writed' => $ok]];
159
160
                    break;
161
162
                default:
163
                    $json = ['status' => false, 'error' => 'unknown_action'];
164
165
                    break;
166
            }
167
        } catch (Throwable $e) {
168
            $json = ['status' => false, 'error' => $e->getMessage()];
169
        }
170
171
        return new JsonResponse($json);
172
    }
173
174
    #[Route(path: '/account/chat', name: 'chamilo_core_global_chat_home', options: ['expose' => true])]
175
    public function globalHome(): Response
176
    {
177
        api_block_anonymous_users();
178
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
179
            return $this->redirectToRoute('homepage');
180
        }
181
182
        return $this->render('@ChamiloCore/Chat/chat.html.twig', []);
183
    }
184
185
    #[Route(path: '/account/chat/api/start', name: 'chamilo_core_chat_api_start', options: ['expose' => true], methods: ['GET'])]
186
    public function globalStart(): JsonResponse
187
    {
188
        api_block_anonymous_users();
189
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
190
            return new JsonResponse(['error' => 'disabled'], 403);
191
        }
192
193
        $chat = new Chat();
194
195
        ob_start();
196
        $ret = $chat->startSession();
197
        $echoed = ob_get_clean();
198
199
        if ('' !== $echoed) {
200
            return JsonResponse::fromJsonString($echoed);
201
        }
202
203
        if (\is_string($ret)) {
204
            return JsonResponse::fromJsonString((string) $ret);
205
        }
206
207
        return new JsonResponse($ret ?? []);
208
    }
209
210
    #[Route(path: '/account/chat/api/contacts', name: 'chamilo_core_chat_api_contacts', options: ['expose' => true], methods: ['POST'])]
211
    public function globalContacts(): Response
212
    {
213
        api_block_anonymous_users();
214
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
215
            return new Response('', 403);
216
        }
217
218
        $chat = new Chat();
219
        $html = $chat->getContacts();
220
221
        return new Response($html, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
222
    }
223
224
    #[Route(path: '/account/chat/api/heartbeat', name: 'chamilo_core_chat_api_heartbeat', options: ['expose' => true], methods: ['GET'])]
225
    public function globalHeartbeat(Request $req): JsonResponse
226
    {
227
        api_block_anonymous_users();
228
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
229
            return new JsonResponse(['error' => 'disabled'], 403);
230
        }
231
232
        $mode = (string) $req->query->get('mode', 'min');
233
        $sinceId = (int) $req->query->get('since_id', 0);
234
        $peerId = (int) $req->query->get('peer_id', 0);
235
236
        // allow client to ask for presence inside the same heartbeat
237
        $presenceRaw = (string) $req->query->get('presence_ids', '');
238
        $presenceIds = $this->parseIdsFromRaw($presenceRaw);
239
240
        // optional contacts refresh flag
241
        $includeContacts = (bool) $req->query->get('include_contacts', false);
242
243
        $chat = new Chat();
244
        $data = [];
245
246
        // Tiny / min modes are now the "normal" unified path
247
        if ('tiny' === $mode && $peerId > 0) {
248
            $data = $chat->heartbeatTiny(api_get_user_id(), $peerId, $sinceId);
249
        } elseif ('min' === $mode) {
250
            $data = $chat->heartbeatMin(api_get_user_id(), $sinceId);
251
        } else {
252
            // Fallback (legacy full heartbeat)
253
            ob_start();
254
            $ret = $chat->heartbeat();
255
            $echoed = ob_get_clean();
256
257
            if ('' !== $echoed) {
258
                return JsonResponse::fromJsonString($echoed);
259
            }
260
261
            if (\is_string($ret)) {
262
                return JsonResponse::fromJsonString($ret);
263
            }
264
265
            $data = \is_array($ret) ? $ret : [];
266
        }
267
268
        // Attach presence map when requested
269
        if (!empty($presenceIds)) {
270
            $data['presence'] = $this->buildPresenceMap($presenceIds);
271
        }
272
273
        // Attach contacts HTML only when explicitly requested
274
        if ($includeContacts) {
275
            $html = $chat->getContacts();
276
            $data['contacts_html'] = \is_string($html) ? $html : '';
277
        }
278
279
        $resp = new JsonResponse($data);
280
        $resp->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
281
282
        return $resp;
283
    }
284
285
    #[Route(
286
        path: '/account/chat/api/history_since',
287
        name: 'chamilo_core_chat_api_history_since',
288
        options: ['expose' => true],
289
        methods: ['GET']
290
    )]
291
    public function globalHistorySince(Request $req): JsonResponse
292
    {
293
        api_block_anonymous_users();
294
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
295
            return new JsonResponse(['error' => 'disabled'], 403);
296
        }
297
298
        $peerId = (int) $req->query->get('user_id', 0);
299
        $sinceId = (int) $req->query->get('since_id', 0);
300
        if ($peerId <= 0) {
301
            return new JsonResponse([]);
302
        }
303
304
        $chat = new Chat();
305
        $items = $chat->getIncomingSince($peerId, api_get_user_id(), $sinceId);
306
        $resp = new JsonResponse($items);
307
        $resp->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
308
309
        return $resp;
310
    }
311
312
    #[Route(path: '/account/chat/api/send', name: 'chamilo_core_chat_api_send', options: ['expose' => true], methods: ['POST'])]
313
    public function globalSend(Request $req): JsonResponse
314
    {
315
        api_block_anonymous_users();
316
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
317
            return new JsonResponse(['error' => 'disabled'], 403);
318
        }
319
320
        $to = (int) $req->request->get('to', 0);
321
        $message = (string) $req->request->get('message', '');
322
        $chat = new Chat();
323
324
        ob_start();
325
        $ret = $chat->send(api_get_user_id(), $to, $message);
326
        $echoed = ob_get_clean();
327
328
        if ('' !== $echoed) {
329
            $trim = trim($echoed);
330
            if (ctype_digit($trim)) {
331
                return new JsonResponse(['id' => (int) $trim]);
332
            }
333
334
            return JsonResponse::fromJsonString($echoed);
335
        }
336
337
        if (\is_string($ret)) {
338
            return JsonResponse::fromJsonString($ret);
339
        }
340
341
        return new JsonResponse($ret ?? ['id' => 0]);
342
    }
343
344
    #[Route(path: '/account/chat/api/status', name: 'chamilo_core_chat_api_status', options: ['expose' => true], methods: ['POST'])]
345
    public function globalStatus(Request $req): JsonResponse
346
    {
347
        api_block_anonymous_users();
348
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
349
            return new JsonResponse(['error' => 'disabled'], 403);
350
        }
351
352
        $status = (int) $req->request->get('status', 0);
353
354
        $chat = new Chat();
355
        $chat->setUserStatus($status);
356
357
        return new JsonResponse(['ok' => true, 'status' => $status]);
358
    }
359
360
    #[Route(path: '/account/chat/api/history', name: 'chamilo_core_chat_api_history', options: ['expose' => true], methods: ['GET'])]
361
    public function globalHistory(Request $req): JsonResponse
362
    {
363
        api_block_anonymous_users();
364
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
365
            return new JsonResponse(['error' => 'disabled'], 403);
366
        }
367
368
        $peerId = (int) $req->query->get('user_id', 0);
369
        $visible = (int) $req->query->get('visible_messages', 0);
370
371
        if (!$peerId) {
372
            return new JsonResponse([]);
373
        }
374
375
        $chat = new Chat();
376
        $items = $chat->getPreviousMessages($peerId, api_get_user_id(), $visible);
377
378
        if (!empty($items)) {
379
            sort($items);
380
381
            return new JsonResponse($items);
382
        }
383
384
        return new JsonResponse([]);
385
    }
386
387
    #[Route(path: '/account/chat/api/preview', name: 'chamilo_core_chat_api_preview', options: ['expose' => true], methods: ['POST'])]
388
    public function globalPreview(Request $req): Response
389
    {
390
        api_block_anonymous_users();
391
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
392
            return new Response('', 403);
393
        }
394
395
        $html = CourseChatUtils::prepareMessage((string) $req->request->get('message', ''));
396
397
        return new Response($html, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
398
    }
399
400
    #[Route(path: '/account/chat/api/presence', name: 'chamilo_core_chat_api_presence', options: ['expose' => true], methods: ['POST'])]
401
    public function globalPresence(Request $req): JsonResponse
402
    {
403
        api_block_anonymous_users();
404
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
405
            return new JsonResponse(['error' => 'disabled'], 403);
406
        }
407
408
        $raw = (string) $req->request->get('ids', '');
409
        $ids = $this->parseIdsFromRaw($raw);
410
411
        $map = $this->buildPresenceMap($ids);
412
413
        return new JsonResponse(['presence' => $map]);
414
    }
415
416
    #[Route(path: '/account/chat/api/ack', name: 'chamilo_core_chat_api_ack', options: ['expose' => true], methods: ['POST'])]
417
    public function globalAck(Request $req): JsonResponse
418
    {
419
        api_block_anonymous_users();
420
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
421
            return new JsonResponse(['error' => 'disabled'], 403);
422
        }
423
424
        $peerId = (int) $req->request->get('peer_id', 0);
425
        $lastSeenId = (int) $req->request->get('last_seen_id', 0);
426
        if ($peerId <= 0 || $lastSeenId <= 0) {
427
            return new JsonResponse(['ok' => false, 'error' => 'bad_params'], 400);
428
        }
429
430
        $chat = new Chat();
431
432
        try {
433
            $n = $chat->ackReadUpTo($peerId, api_get_user_id(), $lastSeenId);
434
435
            return new JsonResponse(['ok' => true, 'updated' => $n]);
436
        } catch (Throwable $e) {
437
            return new JsonResponse(['ok' => false, 'error' => $e->getMessage()], 500);
438
        }
439
    }
440
441
    /**
442
     * @param string $raw Raw "ids" input ("1,2,3" or JSON array)
443
     *
444
     * @return int[]
445
     */
446
    private function parseIdsFromRaw(string $raw): array
447
    {
448
        if ('' === $raw) {
449
            return [];
450
        }
451
452
        $ids = [];
453
        $tryJson = json_decode($raw, true);
454
        if (\is_array($tryJson)) {
455
            $ids = array_filter(array_map('intval', $tryJson));
456
        } else {
457
            $ids = array_filter(array_map('intval', preg_split('/[,\s]+/', $raw)));
458
        }
459
460
        return $ids;
461
    }
462
463
    /**
464
     * Compute presence map for a list of user ids (1 = online, 0 = offline).
465
     *
466
     * @param int[] $ids
467
     */
468
    private function buildPresenceMap(array $ids): array
469
    {
470
        $map = [];
471
472
        foreach ($ids as $id) {
473
            $ui = api_get_user_info($id, true);
474
            $v = $ui['user_is_online_in_chat'] ?? $ui['user_is_online'] ?? $ui['online'] ?? null;
475
            $online = false;
476
477
            if (null !== $v) {
478
                if (\is_string($v)) {
479
                    $online = 1 === preg_match('/^(1|true|online|on)$/i', $v);
480
                } else {
481
                    $online = !empty($v);
482
                }
483
            }
484
485
            if (false === $online && !empty($ui['last_connection'])) {
486
                $ts = api_strtotime($ui['last_connection'], 'UTC');
487
                $online = (time() - $ts) <= 120;
488
            }
489
490
            $map[$id] = $online ? 1 : 0;
491
        }
492
493
        return $map;
494
    }
495
}
496