Passed
Pull Request — master (#7023)
by
unknown
08:28
created

ChatController::globalHome()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 9
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
        /** @var CDocumentRepository $docsRepo */
91
        $docsRepo = $doctrine->getRepository(CDocument::class);
92
93
        $docsRepo->ensureChatSystemFolder($course, $session);
94
        $docRoot = $docsRepo->ensureChatSystemFolderUnderCourseRoot($course, $session);
95
96
        $chat = new CourseChatUtils(
97
            $courseId,
98
            $userId,
99
            $sessionId,
100
            $groupId,
101
            $docRoot,
102
            $convRepo
103
        );
104
105
        $action = (string) $request->get('action', 'track');
106
        $json = ['status' => false];
107
108
        try {
109
            switch ($action) {
110
                case 'chat_logout':
111
                    Event::registerLog([
112
                        'tool' => TOOL_CHAT,
113
                        'action' => 'exit',
114
                        'action_details' => 'exit-chat',
115
                    ]);
116
                    $json = ['status' => true];
117
                    break;
118
119
                case 'track':
120
                    $chat->keepUserAsConnected();
121
                    $chat->disconnectInactiveUsers();
122
123
                    $friend = (int) $request->get('friend', 0);
124
                    $newUsersOnline = $chat->countUsersOnline();
125
                    $oldUsersOnline = (int) $request->get('users_online', 0);
126
127
                    $json = [
128
                        'status' => true,
129
                        'data' => [
130
                            'oldFileSize' => false,
131
                            'history' => $chat->readMessages(false, $friend),
132
                            'usersOnline' => $newUsersOnline,
133
                            'userList' => $newUsersOnline !== $oldUsersOnline ? $chat->listUsersOnline() : null,
134
                            'currentFriend' => $friend,
135
                        ],
136
                    ];
137
                    break;
138
139
                case 'preview':
140
                    $msg = (string) $request->get('message', '');
141
                    $json = ['status' => true, 'data' => ['message' => \CourseChatUtils::prepareMessage($msg)]];
142
                    break;
143
144
                case 'reset':
145
                    $friend = (int) $request->get('friend', 0);
146
                    $json = ['status' => true, 'data' => $chat->readMessages(true, $friend)];
147
                    break;
148
149
                case 'write':
150
                    $friend = (int) $request->get('friend', 0);
151
                    $msg = (string) $request->get('message', '');
152
                    $ok = $chat->saveMessage($msg, $friend);
153
                    $json = ['status' => $ok, 'data' => ['writed' => $ok]];
154
                    break;
155
156
                default:
157
                    $json = ['status' => false, 'error' => 'unknown_action'];
158
                    break;
159
            }
160
        } catch (\Throwable $e) {
161
            $json = ['status' => false, 'error' => $e->getMessage()];
162
        }
163
164
        return new JsonResponse($json);
165
    }
166
167
    #[Route(path: '/account/chat', name: 'chamilo_core_global_chat_home', options: ['expose' => true])]
168
    public function globalHome(): Response
169
    {
170
        api_block_anonymous_users();
171
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
172
            return $this->redirectToRoute('homepage');
173
        }
174
175
        return $this->render('@ChamiloCore/Chat/chat.html.twig', []);
176
    }
177
178
    #[Route(path: '/account/chat/api/start', name: 'chamilo_core_chat_api_start', options: ['expose' => true], methods: ['GET'])]
179
    public function globalStart(): JsonResponse
180
    {
181
        api_block_anonymous_users();
182
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
183
            return new JsonResponse(['error' => 'disabled'], 403);
184
        }
185
186
        $chat = new Chat();
187
188
        ob_start();
189
        $ret = $chat->startSession();
190
        $echoed = ob_get_clean();
191
192
        if ('' !== $echoed) {
193
            return JsonResponse::fromJsonString($echoed);
194
        }
195
196
        if (\is_string($ret)) {
197
            return JsonResponse::fromJsonString((string) $ret);
198
        }
199
200
        return new JsonResponse($ret ?? []);
201
    }
202
203
    #[Route(path: '/account/chat/api/contacts', name: 'chamilo_core_chat_api_contacts', options: ['expose' => true], methods: ['POST'])]
204
    public function globalContacts(): Response
205
    {
206
        api_block_anonymous_users();
207
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
208
            return new Response('', 403);
209
        }
210
211
        $chat = new Chat();
212
        $html = $chat->getContacts();
213
214
        return new Response($html, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
215
    }
216
217
    #[Route(path: '/account/chat/api/heartbeat', name: 'chamilo_core_chat_api_heartbeat', options: ['expose' => true], methods: ['GET'])]
218
    public function globalHeartbeat(Request $req): JsonResponse
219
    {
220
        api_block_anonymous_users();
221
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
222
            return new JsonResponse(['error' => 'disabled'], 403);
223
        }
224
225
        $mode    = (string) $req->query->get('mode', 'min');
226
        $sinceId = (int) $req->query->get('since_id', 0);
227
        $peerId  = (int) $req->query->get('peer_id', 0);
228
229
        $chat = new \Chat();
230
231
        // NEW: ultra-tiny per-peer check (constant-time)
232
        if ($mode === 'tiny' && $peerId > 0) {
233
            $data = $chat->heartbeatTiny(api_get_user_id(), $peerId, $sinceId);
234
            // Force ultra-small JSON and no-store
235
            $resp = new JsonResponse($data);
236
            $resp->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
237
            return $resp;
238
        }
239
240
        if ($mode === 'min') {
241
            $data = $chat->heartbeatMin(api_get_user_id(), $sinceId);
242
            $resp = new JsonResponse($data);
243
            $resp->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
244
            return $resp;
245
        }
246
247
        // Fallback (rare): full heartbeat (legacy)
248
        ob_start();
249
        $ret = $chat->heartbeat();
250
        $echoed = ob_get_clean();
251
        if ($echoed !== '') return JsonResponse::fromJsonString($echoed);
252
        if (is_string($ret)) return JsonResponse::fromJsonString($ret);
253
        return new JsonResponse(is_array($ret) ? $ret : []);
254
    }
255
256
    #[Route(
257
        path: '/account/chat/api/history_since',
258
        name: 'chamilo_core_chat_api_history_since',
259
        options: ['expose' => true],
260
        methods: ['GET']
261
    )]
262
    public function globalHistorySince(Request $req): JsonResponse
263
    {
264
        api_block_anonymous_users();
265
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
266
            return new JsonResponse(['error' => 'disabled'], 403);
267
        }
268
269
        $peerId  = (int) $req->query->get('user_id', 0);
270
        $sinceId = (int) $req->query->get('since_id', 0);
271
        if ($peerId <= 0) return new JsonResponse([]);
272
273
        $chat  = new \Chat();
274
        $items = $chat->getIncomingSince($peerId, api_get_user_id(), $sinceId);
275
        $resp  = new JsonResponse($items);
276
        $resp->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
277
        return $resp;
278
    }
279
280
    #[Route(path: '/account/chat/api/send', name: 'chamilo_core_chat_api_send', options: ['expose' => true], methods: ['POST'])]
281
    public function globalSend(Request $req): JsonResponse
282
    {
283
        api_block_anonymous_users();
284
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
285
            return new JsonResponse(['error' => 'disabled'], 403);
286
        }
287
288
        $to = (int) $req->request->get('to', 0);
289
        $message = (string) $req->request->get('message', '');
290
        $chat = new Chat();
291
292
        ob_start();
293
        $ret = $chat->send(api_get_user_id(), $to, $message);
294
        $echoed = ob_get_clean();
295
296
        if ('' !== $echoed) {
297
            $trim = trim($echoed);
298
            if (ctype_digit($trim)) {
299
                return new JsonResponse(['id' => (int) $trim]);
300
            }
301
            return JsonResponse::fromJsonString($echoed);
302
        }
303
304
        if (\is_string($ret)) {
305
            return JsonResponse::fromJsonString($ret);
306
        }
307
308
        return new JsonResponse($ret ?? ['id' => 0]);
309
    }
310
311
    #[Route(path: '/account/chat/api/status', name: 'chamilo_core_chat_api_status', options: ['expose' => true], methods: ['POST'])]
312
    public function globalStatus(Request $req): JsonResponse
313
    {
314
        api_block_anonymous_users();
315
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
316
            return new JsonResponse(['error' => 'disabled'], 403);
317
        }
318
319
        $status = (int) $req->request->get('status', 0);
320
321
        $chat = new Chat();
322
        $chat->setUserStatus($status);
323
324
        return new JsonResponse(['ok' => true, 'status' => $status]);
325
    }
326
327
    #[Route(path: '/account/chat/api/history', name: 'chamilo_core_chat_api_history', options: ['expose' => true], methods: ['GET'])]
328
    public function globalHistory(Request $req): JsonResponse
329
    {
330
        api_block_anonymous_users();
331
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
332
            return new JsonResponse(['error' => 'disabled'], 403);
333
        }
334
335
        $peerId = (int) $req->query->get('user_id', 0);
336
        $visible = (int) $req->query->get('visible_messages', 0);
337
338
        if (!$peerId) {
339
            return new JsonResponse([]);
340
        }
341
342
        $chat = new Chat();
343
        $items = $chat->getPreviousMessages($peerId, api_get_user_id(), $visible);
344
345
        if (!empty($items)) {
346
            sort($items);
347
348
            return new JsonResponse($items);
349
        }
350
351
        return new JsonResponse([]);
352
    }
353
354
    #[Route(path: '/account/chat/api/preview', name: 'chamilo_core_chat_api_preview', options: ['expose' => true], methods: ['POST'])]
355
    public function globalPreview(Request $req): Response
356
    {
357
        api_block_anonymous_users();
358
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
359
            return new Response('', 403);
360
        }
361
362
        $html = CourseChatUtils::prepareMessage((string) $req->request->get('message', ''));
363
364
        return new Response($html, 200, ['Content-Type' => 'text/html; charset=UTF-8']);
365
    }
366
367
    #[Route(path: '/account/chat/api/presence', name: 'chamilo_core_chat_api_presence', options: ['expose' => true], methods: ['POST'])]
368
    public function globalPresence(Request $req): JsonResponse
369
    {
370
        api_block_anonymous_users();
371
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
372
            return new JsonResponse(['error' => 'disabled'], 403);
373
        }
374
375
        $raw = (string) $req->request->get('ids', '');
376
        $ids = [];
377
        if ('' !== $raw) {
378
            $tryJson = json_decode($raw, true);
379
            if (\is_array($tryJson)) {
380
                $ids = array_filter(array_map('intval', $tryJson));
381
            } else {
382
                $ids = array_filter(array_map('intval', preg_split('/[,\s]+/', $raw)));
383
            }
384
        }
385
386
        $map = [];
387
        foreach ($ids as $id) {
388
            $ui = api_get_user_info($id, true);
389
            $v = $ui['user_is_online_in_chat'] ?? $ui['user_is_online'] ?? $ui['online'] ?? null;
390
            $online = false;
391
            if (null !== $v) {
392
                if (\is_string($v)) {
393
                    $online = 1 === preg_match('/^(1|true|online|on)$/i', $v);
394
                } else {
395
                    $online = !empty($v);
396
                }
397
            }
398
            if (false === $online && !empty($ui['last_connection'])) {
399
                $ts = api_strtotime($ui['last_connection'], 'UTC');
400
                $online = (time() - $ts) <= 120;
401
            }
402
            $map[$id] = $online ? 1 : 0;
403
        }
404
405
        return new JsonResponse(['presence' => $map]);
406
    }
407
408
    #[Route(path: '/account/chat/api/ack', name: 'chamilo_core_chat_api_ack', options: ['expose' => true], methods: ['POST'])]
409
    public function globalAck(Request $req): JsonResponse
410
    {
411
        api_block_anonymous_users();
412
        if ('true' !== api_get_setting('chat.allow_global_chat')) {
413
            return new JsonResponse(['error' => 'disabled'], 403);
414
        }
415
416
        $peerId = (int) $req->request->get('peer_id', 0);
417
        $lastSeenId = (int) $req->request->get('last_seen_id', 0);
418
        if ($peerId <= 0 || $lastSeenId <= 0) {
419
            return new JsonResponse(['ok' => false, 'error' => 'bad_params'], 400);
420
        }
421
422
        $chat = new Chat();
423
424
        try {
425
            $n = $chat->ackReadUpTo($peerId, api_get_user_id(), $lastSeenId);
426
427
            return new JsonResponse(['ok' => true, 'updated' => $n]);
428
        } catch (Throwable $e) {
429
            return new JsonResponse(['ok' => false, 'error' => $e->getMessage()], 500);
430
        }
431
    }
432
}
433