CourseChatUtils::saveMessage()   F
last analyzed

Complexity

Conditions 20
Paths > 20000

Size

Total Lines 164
Code Lines 118

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 118
nc 66154
nop 2
dl 0
loc 164
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\CourseRelUser;
6
use Chamilo\CoreBundle\Entity\ResourceFile;
7
use Chamilo\CoreBundle\Entity\ResourceLink;
8
use Chamilo\CoreBundle\Entity\ResourceNode;
9
use Chamilo\CoreBundle\Entity\Session;
10
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
11
use Chamilo\CoreBundle\Entity\User;
12
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
13
use Chamilo\CoreBundle\Repository\ResourceRepository;
14
use Chamilo\CourseBundle\Entity\CChatConnected;
15
use Chamilo\CourseBundle\Entity\CChatConversation;
16
use Doctrine\Common\Collections\Criteria;
17
use Michelf\MarkdownExtra;
18
use Symfony\Component\HttpFoundation\File\UploadedFile;
19
20
/**
21
 * Course chat utils.
22
 */
23
class CourseChatUtils
24
{
25
    private $groupId;
26
    private $courseId;
27
    private $sessionId;
28
    private $userId;
29
30
    /** @var ResourceNode */
31
    private $resourceNode;
32
33
    /** @var ResourceRepository */
34
    private $repository;
35
36
    /** Debug flag */
37
    private $debug = false;
38
39
    public function __construct($courseId, $userId, $sessionId, $groupId, ResourceNode $resourceNode, ResourceRepository $repository)
40
    {
41
        $this->courseId     = (int) $courseId;
42
        $this->userId       = (int) $userId;
43
        $this->sessionId    = (int) $sessionId;
44
        $this->groupId      = (int) $groupId;
45
        $this->resourceNode = $resourceNode;
46
        $this->repository   = $repository;
47
48
        $this->dbg('construct', [
49
            'courseId'     => $courseId,
50
            'userId'       => $userId,
51
            'sessionId'    => $sessionId,
52
            'groupId'      => $groupId,
53
            'parentNodeId' => $resourceNode->getId() ?? null,
54
            'repo'         => get_class($repository),
55
        ]);
56
    }
57
58
    /** Simple debug helper */
59
    private function dbg(string $msg, array $ctx = []): void
60
    {
61
        if (!$this->debug) { return; }
62
        $line = '[CourseChat] '.$msg;
63
        if ($ctx) { $line .= ' | '.json_encode($ctx, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); }
0 ignored issues
show
Bug Best Practice introduced by
The expression $ctx of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
64
        error_log($line);
65
    }
66
67
    /** Build a slug out of the file title (matches our nodes like: messages-...-log-html) */
68
    private function makeSlug(string $fileTitle): string
69
    {
70
        $slug = strtolower($fileTitle);
71
        $slug = strtr($slug, ['.' => '-']);
72
        $slug = preg_replace('~[^a-z0-9\-\_]+~', '-', $slug);
73
        $slug = preg_replace('~-+~', '-', $slug);
74
        return trim($slug, '-');
75
    }
76
77
    /** Build a base name by day and scope (all / session / group / 1:1) */
78
    private function buildBasename(int $friendId = 0): string
79
    {
80
        $dateNow  = date('Y-m-d');
81
        $basename = 'messages-'.$dateNow;
82
83
        if ($this->groupId && !$friendId) {
84
            $basename .= '_gid-'.$this->groupId;
85
        } elseif ($this->sessionId && !$friendId) {
86
            $basename .= '_sid-'.$this->sessionId;
87
        } elseif ($friendId) {
88
            // stable order for 1:1 (smallest id first)
89
            $basename .= ($this->userId < $friendId)
90
                ? '_uid-'.$this->userId.'-'.$friendId
91
                : '_uid-'.$friendId.'-'.$this->userId;
92
        }
93
        return $basename;
94
    }
95
96
    /** Returns [fileTitle, slug] */
97
    private function buildNames(int $friendId = 0): array
98
    {
99
        $fileTitle = $this->buildBasename($friendId).'-log.html';
100
        $slug      = $this->makeSlug($fileTitle);
101
        return [$fileTitle, $slug];
102
    }
103
104
    /** Create node + conversation + empty file (used only from saveMessage under a lock) */
105
    private function createNodeWithResource(string $fileTitle, string $slug, ResourceNode $parentNode): ResourceNode
0 ignored issues
show
Unused Code introduced by
The method createNodeWithResource() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
106
    {
107
        $em = Database::getManager();
108
109
        $this->dbg('node.create.start', ['slug' => $slug, 'title' => $fileTitle, 'parent' => $parentNode->getId()]);
110
111
        // temporary empty file
112
        $h = tmpfile();
113
        fwrite($h, '');
114
        $meta     = stream_get_meta_data($h);
115
        $uploaded = new UploadedFile($meta['uri'], $fileTitle, 'text/html', null, true);
116
117
        // conversation
118
        $conversation = new CChatConversation();
119
        if (method_exists($conversation, 'setTitle')) {
120
            $conversation->setTitle($fileTitle);
121
        } else {
122
            $conversation->setResourceName($fileTitle);
123
        }
124
        $conversation->setParentResourceNode($parentNode->getId());
125
126
        // node
127
        $node = new ResourceNode();
128
        $node->setTitle($fileTitle);
129
        $node->setSlug($slug);
130
        $node->setResourceType($parentNode->getResourceType());
131
        $node->setCreator(api_get_user_entity(api_get_user_id()));
132
        $node->setParent($parentNode);
133
134
        if (method_exists($conversation, 'setResourceNode')) {
135
            $conversation->setResourceNode($node);
136
        }
137
138
        $em->persist($node);
139
        $em->persist($conversation);
140
141
        // attach file
142
        $this->repository->addFile($conversation, $uploaded);
143
144
        // publish
145
        $course  = api_get_course_entity();
146
        $session = api_get_session_entity();
147
        $group   = api_get_group_entity();
148
        $conversation->setParent($course);
149
        $conversation->addCourseLink($course, $session, $group);
150
151
        $em->flush();
152
153
        $this->dbg('node.create.ok', ['nodeId' => $node->getId()]);
154
155
        return $node;
156
    }
157
158
    /** Sanitize and convert message to safe HTML */
159
    public static function prepareMessage($message): string
160
    {
161
        if (empty($message)) {
162
            return '';
163
        }
164
165
        $message = trim($message);
166
        $message = nl2br($message);
167
        $message = Security::remove_XSS($message);
168
169
        // url -> anchor
170
        $message = preg_replace(
171
            '@((https?://)?([-\w]+\.[-\w\.]+)+\w(:\d+)?(/([-\w/_\.]*(\?\S+)?)?)*)@',
172
            '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
173
            $message
174
        );
175
        // add http:// when missing
176
        $message = preg_replace(
177
            '/<a\s[^>]*href\s*=\s*"((?!https?:\/\/)[^"]*)"[^>]*>/i',
178
            '<a href="http://$1" target="_blank" rel="noopener noreferrer">',
179
            $message
180
        );
181
182
        $message = MarkdownExtra::defaultTransform($message);
183
184
        return $message;
185
    }
186
187
    /**
188
     * Return the latest node (by id DESC) matching title or slug under the same parent.
189
     * Read-only; does not create.
190
     */
191
    private function findExistingNode(string $fileTitle, string $slug, ResourceNode $parentNode): ?ResourceNode
192
    {
193
        $em = \Database::getManager();
194
        $nodeRepo = $em->getRepository(ResourceNode::class);
195
196
        // latest by exact title
197
        $node = $nodeRepo->findOneBy(
198
            ['title' => $fileTitle, 'parent' => $parentNode],
199
            ['id' => 'DESC']
200
        );
201
        if ($node) { return $node; }
202
203
        // latest by exact slug
204
        return $nodeRepo->findOneBy(
205
            ['slug' => $slug, 'parent' => $parentNode],
206
            ['id' => 'DESC']
207
        );
208
    }
209
210
    /**
211
     * Append a message to the last daily file (chronological order).
212
     */
213
    public function saveMessage($message, $friendId = 0)
214
    {
215
        $this->dbg('saveMessage.in', ['friendId' => (int)$friendId, 'rawLen' => strlen((string)$message)]);
216
        if (!is_string($message) || trim($message) === '') { return false; }
217
218
        // names (one file per day/scope)
219
        [$fileTitle, $slug] = $this->buildNames((int)$friendId);
220
221
        $em = Database::getManager();
222
        /** @var ResourceNodeRepository $nodeRepo */
223
        $nodeRepo = $em->getRepository(ResourceNode::class);
224
        $convRepo = $em->getRepository(CChatConversation::class);
225
        $rfRepo   = $em->getRepository(ResourceFile::class);
226
227
        // parent (chat root)
228
        $parent = $nodeRepo->find($this->resourceNode->getId());
229
        if (!$parent) { $this->dbg('saveMessage.error.noParent'); return false; }
230
231
        // serialize writers for the same daily file (parent+slug)
232
        $lockPath = sys_get_temp_dir().'/ch_chat_lock_'.$parent->getId().'_'.$slug.'.lock';
233
        $lockH = @fopen($lockPath, 'c');
234
        if ($lockH) { @flock($lockH, LOCK_EX); }
0 ignored issues
show
introduced by
$lockH is of type false|resource, thus it always evaluated to false.
Loading history...
Security Best Practice introduced by
It seems like you do not handle an error condition for flock(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

234
        if ($lockH) { /** @scrutinizer ignore-unhandled */ @flock($lockH, LOCK_EX); }

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
235
236
        try {
237
            // latest node for this day/scope (title OR slug)
238
            $qb = $em->createQueryBuilder();
239
            $qb->select('n')
240
                ->from(ResourceNode::class, 'n')
241
                ->where('n.parent = :parent AND (n.title = :title OR n.slug = :slug)')
242
                ->setParameter('parent', $parent)
243
                ->setParameter('title', $fileTitle)
244
                ->setParameter('slug',  $slug)
245
                ->orderBy('n.createdAt', 'DESC')
246
                ->addOrderBy('n.id', 'DESC')
247
                ->setMaxResults(1);
248
            /** @var ResourceNode|null $node */
249
            $node = $qb->getQuery()->getOneOrNullResult();
250
251
            // create node + conversation once
252
            if (!$node) {
253
                $conversation = new CChatConversation();
254
                (method_exists($conversation, 'setTitle')
255
                    ? $conversation->setTitle($fileTitle)
256
                    : $conversation->setResourceName($fileTitle));
257
                $conversation->setParentResourceNode($parent->getId());
258
259
                $node = new ResourceNode();
260
                $node->setTitle($fileTitle);
261
                $node->setSlug($slug);
262
                $node->setResourceType($parent->getResourceType());
263
                $node->setCreator(api_get_user_entity(api_get_user_id()));
264
                $node->setParent($parent);
265
266
                if (method_exists($conversation, 'setResourceNode')) {
267
                    $conversation->setResourceNode($node);
268
                }
269
270
                $em->persist($node);
271
                $em->persist($conversation);
272
273
                $course  = api_get_course_entity();
274
                $session = api_get_session_entity();
275
                $group   = api_get_group_entity();
276
                $conversation->setParent($course);
277
                $conversation->addCourseLink(
278
                    $course, $session, $group
279
                );
280
281
                $em->flush();
282
            }
283
284
            // ensure conversation exists for node
285
            $conversation = $convRepo->findOneBy(['resourceNode' => $node]);
286
            if (!$conversation) {
287
                $conversation = new CChatConversation();
288
                (method_exists($conversation, 'setTitle')
289
                    ? $conversation->setTitle($fileTitle)
290
                    : $conversation->setResourceName($fileTitle));
291
                $conversation->setParentResourceNode($parent->getId());
292
                if (method_exists($conversation, 'setResourceNode')) {
293
                    $conversation->setResourceNode($node);
294
                }
295
                $em->persist($conversation);
296
297
                $course  = api_get_course_entity();
298
                $session = api_get_session_entity();
299
                $group   = api_get_group_entity();
300
                $conversation->setParent($course);
301
                $conversation->addCourseLink(
302
                    $course, $session, $group
303
                );
304
305
                $em->flush();
306
            }
307
308
            // build message bubble
309
            $user      = api_get_user_entity($this->userId);
310
            $isMaster  = api_is_course_admin();
311
            $timeNow   = date('d/m/y H:i:s');
312
            $userPhoto = \UserManager::getUserPicture($this->userId);
313
            $htmlMsg   = self::prepareMessage($message);
314
315
            $bubble = $isMaster
316
                ? '<div class="message-teacher"><div class="content-message"><div class="chat-message-block-name">'
317
                .\UserManager::formatUserFullName($user).'</div><div class="chat-message-block-content">'
318
                .$htmlMsg.'</div><div class="message-date">'.$timeNow
319
                .'</div></div><div class="icon-message"></div><img class="chat-image" src="'.$userPhoto.'"></div>'
320
                : '<div class="message-student"><img class="chat-image" src="'.$userPhoto.'"><div class="icon-message"></div>'
321
                .'<div class="content-message"><div class="chat-message-block-name">'.\UserManager::formatUserFullName($user)
322
                .'</div><div class="chat-message-block-content">'.$htmlMsg.'</div><div class="message-date">'
323
                .$timeNow.'</div></div></div>';
324
325
            // always target latest ResourceFile for today (by id desc)
326
            $rfQb = $em->createQueryBuilder();
327
            $rf   = $rfQb->select('rf')
328
                ->from(ResourceFile::class, 'rf')
329
                ->where('rf.resourceNode = :node AND rf.originalName = :name')
330
                ->setParameter('node', $node)
331
                ->setParameter('name', $fileTitle)
332
                ->orderBy('rf.id', 'DESC')
333
                ->setMaxResults(1)
334
                ->getQuery()
335
                ->getOneOrNullResult();
336
337
            // read current content and append
338
            $existing = '';
339
            if ($rf) {
340
                try { $existing = $nodeRepo->getResourceNodeFileContent($node, $rf) ?? ''; }
341
                catch (\Throwable $e) { $existing = ''; }
342
            }
343
            $newContent = $existing.$bubble;
344
345
            // write back (reuse same physical path)
346
            if ($rf) {
347
                $fs       = $nodeRepo->getFileSystem();
348
                $fileName = $nodeRepo->getFilename($rf);
349
                if ($fs->fileExists($fileName)) { $fs->delete($fileName); }
350
                $fs->write($fileName, $newContent);
351
                if (method_exists($rf, 'setSize')) { $rf->setSize(strlen($newContent)); $em->persist($rf); }
352
                $em->flush();
353
            } else {
354
                // first write of the day → create the ResourceFile with the whole content
355
                if (method_exists($this->repository, 'addFileFromString')) {
356
                    $this->repository->addFileFromString($conversation, $fileTitle, 'text/html', $newContent, true);
357
                    $em->flush();
358
                } else {
359
                    $h = tmpfile(); fwrite($h, $newContent);
360
                    $meta = stream_get_meta_data($h);
361
                    $uploaded = new UploadedFile(
362
                        $meta['uri'], $fileTitle, 'text/html', null, true
363
                    );
364
                    $this->repository->addFile($conversation, $uploaded);
365
                    $em->flush();
366
                }
367
            }
368
369
            $this->dbg('saveMessage.append.ok', ['nodeId' => $node->getId(), 'bytes' => strlen($newContent)]);
370
            return true;
371
372
        } catch (\Throwable $e) {
373
            $this->dbg('saveMessage.error', ['err' => $e->getMessage()]);
374
            return false;
375
        } finally {
376
            if ($lockH) { @flock($lockH, LOCK_UN); @fclose($lockH); }
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

376
            if ($lockH) { @flock($lockH, LOCK_UN); /** @scrutinizer ignore-unhandled */ @fclose($lockH); }

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
introduced by
$lockH is of type false|resource, thus it always evaluated to false.
Loading history...
377
        }
378
    }
379
380
    /**
381
     * Read the last daily file HTML (optionally reset it).
382
     */
383
    public function readMessages($reset = false, $friendId = 0)
384
    {
385
        [$fileTitle, $slug] = $this->buildNames((int)$friendId);
386
387
        $this->dbg('readMessages.in', [
388
            'friendId' => (int)$friendId,
389
            'reset'    => (bool)$reset,
390
            'file'     => $fileTitle,
391
            'slug'     => $slug,
392
        ]);
393
394
        $em = \Database::getManager();
395
        /** @var ResourceNodeRepository $nodeRepo */
396
        $nodeRepo = $em->getRepository(ResourceNode::class);
397
398
        $parent = $nodeRepo->find($this->resourceNode->getId());
399
        if (!$parent) { $this->dbg('readMessages.error.noParent'); return ''; }
400
401
        // read-only: do not create
402
        $node = $this->findExistingNode($fileTitle, $slug, $parent);
403
        if (!$node) { $this->dbg('readMessages.notfound'); return ''; }
404
405
        // locate the same ResourceFile by originalName (latest id desc)
406
        $rfRepo = $em->getRepository(ResourceFile::class);
407
        /** @var ResourceFile|null $rf */
408
        $rf = $rfRepo->findOneBy(
409
            ['resourceNode' => $node, 'originalName' => $fileTitle],
410
            ['id' => 'DESC']
411
        );
412
413
        // optional reset
414
        if ($reset) {
415
            $target = $rf ?: ($node->getResourceFiles()->first() ?: null);
416
            if ($target) {
417
                $fs       = $nodeRepo->getFileSystem();
418
                $fileName = $nodeRepo->getFilename($target);
419
                if ($fs->fileExists($fileName)) {
420
                    $fs->delete($fileName);
421
                    $fs->write($fileName, '');
422
                }
423
                if (method_exists($target, 'setSize')) { $target->setSize(0); $em->persist($target); }
424
                $em->flush();
425
                $this->dbg('readMessages.reset.ok', ['nodeId' => $node->getId(), 'rfId' => $target->getId()]);
426
            }
427
        }
428
429
        try {
430
            // primary: exact RF by originalName
431
            if ($rf) {
432
                $html = $nodeRepo->getResourceNodeFileContent($node, $rf);
433
                $this->dbg('readMessages.out.byOriginalName', [
434
                    'nodeId' => $node->getId(),
435
                    'rfId'   => $rf->getId(),
436
                    'bytes'  => strlen($html ?? ''),
437
                ]);
438
                return $html ?? '';
439
            }
440
441
            // fallback: first attached file (covers legacy hashed names)
442
            $html = $nodeRepo->getResourceNodeFileContent($node);
443
            $this->dbg('readMessages.out.fallbackFirst', [
444
                'nodeId' => $node->getId(),
445
                'bytes'  => strlen($html ?? ''),
446
            ]);
447
            return $html ?? '';
448
449
        } catch (\Throwable $e) {
450
            $this->dbg('readMessages.read.error', ['err' => $e->getMessage()]);
451
            return '';
452
        }
453
    }
454
455
    /** Force a user to exit all course chat connections */
456
    public static function exitChat($userId)
457
    {
458
        $listCourse = CourseManager::get_courses_list_by_user_id($userId);
459
        foreach ($listCourse as $course) {
460
            Database::getManager()
461
                ->createQuery('
462
                    DELETE FROM ChamiloCourseBundle:CChatConnected ccc
463
                    WHERE ccc.cId = :course AND ccc.userId = :user
464
                ')
465
                ->execute([
466
                    'course' => intval($course['real_id']),
467
                    'user'   => intval($userId),
468
                ]);
469
        }
470
    }
471
472
    /** Remove inactive connections (simple heartbeat) */
473
    public function disconnectInactiveUsers(): void
474
    {
475
        $em = Database::getManager();
476
        $extraCondition = $this->groupId
477
            ? "AND ccc.toGroupId = {$this->groupId}"
478
            : "AND ccc.sessionId = {$this->sessionId}";
479
480
        $connectedUsers = $em
481
            ->createQuery("
482
                SELECT ccc FROM ChamiloCourseBundle:CChatConnected ccc
483
                WHERE ccc.cId = :course $extraCondition
484
            ")
485
            ->setParameter('course', $this->courseId)
486
            ->getResult();
487
488
        $now  = new \DateTime(api_get_utc_datetime(), new \DateTimeZone('UTC'));
489
        $nowTs = $now->getTimestamp();
490
491
        /** @var CChatConnected $connection */
492
        foreach ($connectedUsers as $connection) {
493
            $lastTs = $connection->getLastConnection()->getTimestamp();
494
            if (0 !== strcmp($now->format('Y-m-d'), $connection->getLastConnection()->format('Y-m-d'))) {
495
                continue;
496
            }
497
            if (($nowTs - $lastTs) <= 5) {
498
                continue;
499
            }
500
501
            $em
502
                ->createQuery('
503
                    DELETE FROM ChamiloCourseBundle:CChatConnected ccc
504
                    WHERE ccc.cId = :course AND ccc.userId = :user AND ccc.toGroupId = :group
505
                ')
506
                ->execute([
507
                    'course' => $this->courseId,
508
                    'user'   => $connection->getUserId(),
509
                    'group'  => $this->groupId,
510
                ]);
511
        }
512
    }
513
514
    /** Keep (or create) the "connected" record for current user */
515
    public function keepUserAsConnected(): void
516
    {
517
        $em = Database::getManager();
518
        $extraCondition = $this->groupId
519
            ? 'AND ccc.toGroupId = '.$this->groupId
520
            : 'AND ccc.sessionId = '.$this->sessionId;
521
522
        $currentTime = new \DateTime(api_get_utc_datetime(), new \DateTimeZone('UTC'));
523
524
        /** @var CChatConnected|null $connection */
525
        $connection = $em
526
            ->createQuery("
527
                SELECT ccc FROM ChamiloCourseBundle:CChatConnected ccc
528
                WHERE ccc.userId = :user AND ccc.cId = :course $extraCondition
529
            ")
530
            ->setParameters([
531
                'user'   => $this->userId,
532
                'course' => $this->courseId,
533
            ])
534
            ->getOneOrNullResult();
535
536
        if ($connection) {
537
            $connection->setLastConnection($currentTime);
538
            $em->persist($connection);
539
            $em->flush();
540
            return;
541
        }
542
543
        $connection = new CChatConnected();
544
        $connection
545
            ->setCId($this->courseId)
546
            ->setUserId($this->userId)
547
            ->setLastConnection($currentTime)
548
            ->setSessionId($this->sessionId)
549
            ->setToGroupId($this->groupId);
550
551
        $em->persist($connection);
552
        $em->flush();
553
    }
554
555
    /** Legacy helper (kept for BC) */
556
    public function getFileName($absolute = false, $friendId = 0): string
557
    {
558
        $base = $this->buildBasename((int)$friendId).'.log.html';
559
        if (!$absolute) { return $base; }
560
561
        $document_path = '/document';
562
        $chatPath = $document_path.'/chat_files/';
563
564
        if ($this->groupId) {
565
            $group_info = GroupManager::get_group_properties($this->groupId);
566
            $chatPath = $document_path.$group_info['directory'].'/chat_files/';
567
        }
568
569
        return $chatPath.$base;
570
    }
571
572
    /** Count users online (simple 5s heartbeat window) */
573
    public function countUsersOnline(): int
574
    {
575
        $date = new \DateTime(api_get_utc_datetime(), new \DateTimeZone('UTC'));
576
        $date->modify('-5 seconds');
577
578
        $extraCondition = $this->groupId
579
            ? 'AND ccc.toGroupId = '.$this->groupId
580
            : 'AND ccc.sessionId = '.$this->sessionId;
581
582
        $number = Database::getManager()
583
            ->createQuery("
584
                SELECT COUNT(ccc.userId) FROM ChamiloCourseBundle:CChatConnected ccc
585
                WHERE ccc.lastConnection > :date AND ccc.cId = :course $extraCondition
586
            ")
587
            ->setParameters([
588
                'date'   => $date,
589
                'course' => $this->courseId,
590
            ])
591
            ->getSingleScalarResult();
592
593
        return (int) $number;
594
    }
595
596
    /** Return basic info for connected/eligible users */
597
    public function listUsersOnline(): array
598
    {
599
        $subscriptions = $this->getUsersSubscriptions();
600
        $usersInfo = [];
601
602
        if ($this->groupId) {
603
            /** @var User $groupUser */
604
            foreach ($subscriptions as $groupUser) {
605
                $usersInfo[] = $this->formatUser($groupUser, $groupUser->getStatus());
606
            }
607
        } else {
608
            /** @var CourseRelUser|SessionRelCourseRelUser $subscription */
609
            foreach ($subscriptions as $subscription) {
610
                $user = $subscription->getUser();
611
                $usersInfo[] = $this->formatUser(
612
                    $user,
613
                    $this->sessionId ? $user->getStatus() : $subscription->getStatus()
614
                );
615
            }
616
        }
617
618
        return $usersInfo;
619
    }
620
621
    /** Normalize user card info */
622
    private function formatUser(User $user, $status): array
623
    {
624
        return [
625
            'id'            => $user->getId(),
626
            'firstname'     => $user->getFirstname(),
627
            'lastname'      => $user->getLastname(),
628
            'status'        => $status,
629
            'image_url'     => UserManager::getUserPicture($user->getId()),
630
            'profile_url'   => api_get_path(WEB_CODE_PATH).'social/profile.php?u='.$user->getId(),
631
            'complete_name' => UserManager::formatUserFullName($user),
632
            'username'      => $user->getUsername(),
633
            'email'         => $user->getEmail(),
634
            'isConnected'   => $this->userIsConnected($user->getId()),
635
        ];
636
    }
637
638
    /** Fetch subscriptions (course / session / group) */
639
    private function getUsersSubscriptions()
640
    {
641
        $em = Database::getManager();
642
643
        if ($this->groupId) {
644
            $students = $em
645
                ->createQuery(
646
                    'SELECT u FROM ChamiloCoreBundle:User u
647
                     INNER JOIN ChamiloCourseBundle:CGroupRelUser gru
648
                        WITH u.id = gru.userId AND gru.cId = :course
649
                     WHERE u.id != :user AND gru.groupId = :group
650
                       AND u.active = true'
651
                )
652
                ->setParameters(['course' => $this->courseId, 'user' => $this->userId, 'group' => $this->groupId])
653
                ->getResult();
654
655
            $tutors = $em
656
                ->createQuery(
657
                    'SELECT u FROM ChamiloCoreBundle:User u
658
                     INNER JOIN ChamiloCourseBundle:CGroupRelTutor grt
659
                        WITH u.id = grt.userId AND grt.cId = :course
660
                     WHERE u.id != :user AND grt.groupId = :group
661
                       AND u.active = true'
662
                )
663
                ->setParameters(['course' => $this->courseId, 'user' => $this->userId, 'group' => $this->groupId])
664
                ->getResult();
665
666
            return array_merge($tutors, $students);
667
        }
668
669
        $course = api_get_course_entity($this->courseId);
670
671
        if ($this->sessionId) {
672
            $session   = api_get_session_entity($this->sessionId);
673
            $criteria  = Criteria::create()->where(Criteria::expr()->eq('course', $course));
674
            $userCoach = api_is_course_session_coach($this->userId, $course->getId(), $session->getId());
675
676
            if ('true' === api_get_setting('chat.course_chat_restrict_to_coach')) {
677
                if ($userCoach) {
678
                    $criteria->andWhere(Criteria::expr()->eq('status', Session::STUDENT));
679
                } else {
680
                    $criteria->andWhere(Criteria::expr()->eq('status', Session::COURSE_COACH));
681
                }
682
            }
683
684
            $criteria->orderBy(['status' => Criteria::DESC]);
685
686
            return $session
687
                ->getUserCourseSubscriptions()
0 ignored issues
show
Bug introduced by
The method getUserCourseSubscriptions() does not exist on Chamilo\CoreBundle\Entity\Session. ( Ignorable by Annotation )

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

687
                ->/** @scrutinizer ignore-call */ getUserCourseSubscriptions()

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...
688
                ->matching($criteria)
689
                ->filter(function (SessionRelCourseRelUser $scru) {
690
                    return $scru->getUser()->isActive();
691
                });
692
        }
693
694
        return $course
695
            ->getUsers()
696
            ->filter(function (CourseRelUser $cru) {
697
                return $cru->getUser()->isActive();
698
            });
699
    }
700
701
    /** Quick online check for one user */
702
    private function userIsConnected($userId): int
703
    {
704
        $date = new \DateTime(api_get_utc_datetime(), new \DateTimeZone('UTC'));
705
        $date->modify('-5 seconds');
706
707
        $extraCondition = $this->groupId
708
            ? 'AND ccc.toGroupId = '.$this->groupId
709
            : 'AND ccc.sessionId = '.$this->sessionId;
710
711
        $number = Database::getManager()
712
            ->createQuery("
713
                SELECT COUNT(ccc.userId) FROM ChamiloCourseBundle:CChatConnected ccc
714
                WHERE ccc.lastConnection > :date AND ccc.cId = :course AND ccc.userId = :user $extraCondition
715
            ")
716
            ->setParameters([
717
                'date'   => $date,
718
                'course' => $this->courseId,
719
                'user'   => $userId,
720
            ])
721
            ->getSingleScalarResult();
722
723
        return (int) $number;
724
    }
725
}
726