Passed
Push — master ( 16faf8...a2e602 )
by
unknown
23:21 queued 14:59
created

CourseChatUtils::makeSlug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 function prepareMessage($message)
160
    {
161
        $this->dbg('prepareMessage.in', ['len' => strlen((string) $message)]);
162
        if (empty($message)) {
163
            return '';
164
        }
165
166
        $message = trim($message);
167
        $message = nl2br($message);
168
        $message = Security::remove_XSS($message);
169
170
        // url -> anchor
171
        $message = preg_replace(
172
            '@((https?://)?([-\w]+\.[-\w\.]+)+\w(:\d+)?(/([-\w/_\.]*(\?\S+)?)?)*)@',
173
            '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
174
            $message
175
        );
176
        // add http:// when missing
177
        $message = preg_replace(
178
            '/<a\s[^>]*href\s*=\s*"((?!https?:\/\/)[^"]*)"[^>]*>/i',
179
            '<a href="http://$1" target="_blank" rel="noopener noreferrer">',
180
            $message
181
        );
182
183
        $message = MarkdownExtra::defaultTransform($message);
184
        $this->dbg('prepareMessage.out', ['len' => strlen($message)]);
185
186
        return $message;
187
    }
188
189
    /**
190
     * Return the latest node (by id DESC) matching title or slug under the same parent.
191
     * Read-only; does not create.
192
     */
193
    private function findExistingNode(string $fileTitle, string $slug, ResourceNode $parentNode): ?ResourceNode
194
    {
195
        $em = \Database::getManager();
196
        $nodeRepo = $em->getRepository(ResourceNode::class);
197
198
        // latest by exact title
199
        $node = $nodeRepo->findOneBy(
200
            ['title' => $fileTitle, 'parent' => $parentNode],
201
            ['id' => 'DESC']
202
        );
203
        if ($node) { return $node; }
204
205
        // latest by exact slug
206
        return $nodeRepo->findOneBy(
207
            ['slug' => $slug, 'parent' => $parentNode],
208
            ['id' => 'DESC']
209
        );
210
    }
211
212
    /**
213
     * Append a message to the last daily file (chronological order).
214
     */
215
    public function saveMessage($message, $friendId = 0)
216
    {
217
        $this->dbg('saveMessage.in', ['friendId' => (int)$friendId, 'rawLen' => strlen((string)$message)]);
218
        if (!is_string($message) || trim($message) === '') { return false; }
219
220
        // names (one file per day/scope)
221
        [$fileTitle, $slug] = $this->buildNames((int)$friendId);
222
223
        $em = Database::getManager();
224
        /** @var ResourceNodeRepository $nodeRepo */
225
        $nodeRepo = $em->getRepository(ResourceNode::class);
226
        $convRepo = $em->getRepository(CChatConversation::class);
227
        $rfRepo   = $em->getRepository(ResourceFile::class);
228
229
        // parent (chat root)
230
        $parent = $nodeRepo->find($this->resourceNode->getId());
231
        if (!$parent) { $this->dbg('saveMessage.error.noParent'); return false; }
232
233
        // serialize writers for the same daily file (parent+slug)
234
        $lockPath = sys_get_temp_dir().'/ch_chat_lock_'.$parent->getId().'_'.$slug.'.lock';
235
        $lockH = @fopen($lockPath, 'c');
236
        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

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

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

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