Passed
Pull Request — master (#6053)
by
unknown
07:47
created

UserRepository::updatePassword()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Repository\Node;
8
9
use Chamilo\CoreBundle\Entity\AccessUrl;
10
use Chamilo\CoreBundle\Entity\Course;
11
use Chamilo\CoreBundle\Entity\CourseRelUser;
12
use Chamilo\CoreBundle\Entity\ExtraField;
13
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
14
use Chamilo\CoreBundle\Entity\Message;
15
use Chamilo\CoreBundle\Entity\ResourceNode;
16
use Chamilo\CoreBundle\Entity\Session;
17
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
18
use Chamilo\CoreBundle\Entity\Tag;
19
use Chamilo\CoreBundle\Entity\TrackELogin;
20
use Chamilo\CoreBundle\Entity\TrackEOnline;
21
use Chamilo\CoreBundle\Entity\User;
22
use Chamilo\CoreBundle\Entity\Usergroup;
23
use Chamilo\CoreBundle\Entity\UsergroupRelUser;
24
use Chamilo\CoreBundle\Entity\UserRelTag;
25
use Chamilo\CoreBundle\Entity\UserRelUser;
26
use Chamilo\CoreBundle\Repository\ResourceRepository;
27
use Chamilo\CourseBundle\Entity\CGroupRelUser;
28
use Datetime;
29
use Doctrine\Common\Collections\Collection;
30
use Doctrine\Common\Collections\Criteria;
31
use Doctrine\DBAL\Types\Types;
32
use Doctrine\ORM\Query\Expr\Join;
33
use Doctrine\ORM\QueryBuilder;
34
use Doctrine\Persistence\ManagerRegistry;
35
use Exception;
36
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
37
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
38
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
39
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
40
use Symfony\Contracts\Translation\TranslatorInterface;
41
42
use const MB_CASE_LOWER;
43
44
class UserRepository extends ResourceRepository implements PasswordUpgraderInterface
45
{
46
    protected ?UserPasswordHasherInterface $hasher = null;
47
48
    public const USER_IMAGE_SIZE_SMALL = 1;
49
    public const USER_IMAGE_SIZE_MEDIUM = 2;
50
    public const USER_IMAGE_SIZE_BIG = 3;
51
    public const USER_IMAGE_SIZE_ORIGINAL = 4;
52
53
    public function __construct(
54
        ManagerRegistry $registry,
55
        private readonly IllustrationRepository $illustrationRepository,
56
        private readonly TranslatorInterface $translator
57
    ) {
58
        parent::__construct($registry, User::class);
59
    }
60
61
    public function loadUserByIdentifier(string $identifier): ?User
62
    {
63
        return $this->findOneBy([
64
            'username' => $identifier,
65
        ]);
66
    }
67
68
    public function setHasher(UserPasswordHasherInterface $hasher): void
69
    {
70
        $this->hasher = $hasher;
71
    }
72
73
    public function createUser(): User
74
    {
75
        return new User();
76
    }
77
78
    public function updateUser(User $user, bool $andFlush = true): void
79
    {
80
        $this->updateCanonicalFields($user);
81
        $this->updatePassword($user);
82
        $this->getEntityManager()->persist($user);
83
        if ($andFlush) {
84
            $this->getEntityManager()->flush();
85
        }
86
    }
87
88
    public function canonicalize(string $string): string
89
    {
90
        $encoding = mb_detect_encoding($string, mb_detect_order(), true);
91
92
        return $encoding
93
            ? mb_convert_case($string, MB_CASE_LOWER, $encoding)
94
            : mb_convert_case($string, MB_CASE_LOWER);
95
    }
96
97
    public function updateCanonicalFields(User $user): void
98
    {
99
        $user->setUsernameCanonical($this->canonicalize($user->getUsername()));
100
        $user->setEmailCanonical($this->canonicalize($user->getEmail()));
101
    }
102
103
    public function updatePassword(User $user): void
104
    {
105
        $password = (string) $user->getPlainPassword();
106
        if ('' !== $password) {
107
            $password = $this->hasher->hashPassword($user, $password);
0 ignored issues
show
Bug introduced by
The method hashPassword() does not exist on null. ( Ignorable by Annotation )

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

107
            /** @scrutinizer ignore-call */ 
108
            $password = $this->hasher->hashPassword($user, $password);

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...
108
            $user->setPassword($password);
109
            $user->eraseCredentials();
110
        }
111
    }
112
113
    public function isPasswordValid(User $user, string $plainPassword): bool
114
    {
115
        return $this->hasher->isPasswordValid($user, $plainPassword);
116
    }
117
118
    public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
119
    {
120
        /** @var User $user */
121
        $user->setPassword($newHashedPassword);
122
        $this->getEntityManager()->persist($user);
123
        $this->getEntityManager()->flush();
124
    }
125
126
    public function getRootUser(): User
127
    {
128
        $qb = $this->createQueryBuilder('u');
129
        $qb
130
            ->innerJoin(
131
                'u.resourceNode',
132
                'r'
133
            )
134
        ;
135
        $qb
136
            ->where('r.creator = u')
137
            ->andWhere('r.parent IS NULL')
138
            ->getFirstResult()
139
        ;
140
141
        $rootUser = $qb->getQuery()->getSingleResult();
142
143
        if (null === $rootUser) {
144
            throw new UserNotFoundException('Root user not found');
145
        }
146
147
        return $rootUser;
148
    }
149
150
    public function deleteUser(User $user, bool $destroy = false): void
151
    {
152
        $connection = $this->getEntityManager()->getConnection();
153
        $connection->beginTransaction();
154
155
        try {
156
            if ($destroy) {
157
                // Call method to delete messages and attachments
158
                $this->deleteUserMessagesAndAttachments($user);
159
160
                $fallbackUser = $this->getFallbackUser();
161
162
                if ($fallbackUser) {
163
                    $this->reassignUserResourcesToFallbackSQL($user, $fallbackUser, $connection);
164
                }
165
166
                // Remove group relationships
167
                $connection->executeStatement(
168
                    'DELETE FROM usergroup_rel_user WHERE user_id = :userId',
169
                    ['userId' => $user->getId()]
170
                );
171
172
                // Remove resource node if exists
173
                $connection->executeStatement(
174
                    'DELETE FROM resource_node WHERE id = :nodeId',
175
                    ['nodeId' => $user->getResourceNode()->getId()]
176
                );
177
178
                // Remove the user itself
179
                $connection->executeStatement(
180
                    'DELETE FROM user WHERE id = :userId',
181
                    ['userId' => $user->getId()]
182
                );
183
            } else {
184
                // Soft delete the user
185
                $connection->executeStatement(
186
                    'UPDATE user SET active = :softDeleted WHERE id = :userId',
187
                    ['softDeleted' => User::SOFT_DELETED, 'userId' => $user->getId()]
188
                );
189
            }
190
191
            $connection->commit();
192
        } catch (Exception $e) {
193
            $connection->rollBack();
194
195
            throw $e;
196
        }
197
    }
198
199
    /**
200
     * Reassigns resources and related data from a deleted user to a fallback user in the database.
201
     *
202
     * @param mixed $connection
203
     */
204
    protected function reassignUserResourcesToFallbackSQL(User $userToDelete, User $fallbackUser, $connection): void
205
    {
206
        // Update resource nodes created by the user
207
        $connection->executeStatement(
208
            'UPDATE resource_node SET creator_id = :fallbackUserId WHERE creator_id = :userId',
209
            ['fallbackUserId' => $fallbackUser->getId(), 'userId' => $userToDelete->getId()]
210
        );
211
212
        // Update child resource nodes
213
        $connection->executeStatement(
214
            'UPDATE resource_node SET parent_id = :fallbackParentId WHERE parent_id = :userParentId',
215
            [
216
                'fallbackParentId' => $fallbackUser->getResourceNode()?->getId(),
217
                'userParentId' => $userToDelete->getResourceNode()->getId(),
218
            ]
219
        );
220
221
        // Relations to update or delete
222
        $relations = $this->getRelations();
223
224
        foreach ($relations as $relation) {
225
            $table = $relation['table'];
226
            $field = $relation['field'];
227
            $action = $relation['action'];
228
229
            if ('delete' === $action) {
230
                $connection->executeStatement(
231
                    "DELETE FROM $table WHERE $field = :userId",
232
                    ['userId' => $userToDelete->getId()]
233
                );
234
            } elseif ('update' === $action) {
235
                $connection->executeStatement(
236
                    "UPDATE $table SET $field = :fallbackUserId WHERE $field = :userId",
237
                    [
238
                        'fallbackUserId' => $fallbackUser->getId(),
239
                        'userId' => $userToDelete->getId(),
240
                    ]
241
                );
242
            }
243
        }
244
    }
245
246
    /**
247
     * Provides a list of database table relations and their respective actions
248
     * (update or delete) for handling user resource reassignment or deletion.
249
     *
250
     * Any new database table that stores references to users and requires updates
251
     * or deletions when a user is removed should be added to this list. This ensures
252
     * proper handling of dependencies and avoids orphaned data.
253
     */
254
    protected function getRelations(): array
255
    {
256
        return [
257
            ['table' => 'access_url_rel_user', 'field' => 'user_id', 'action' => 'delete'],
258
            ['table' => 'admin', 'field' => 'user_id', 'action' => 'delete'],
259
            ['table' => 'attempt_feedback', 'field' => 'user_id', 'action' => 'update'],
260
            ['table' => 'chat', 'field' => 'to_user', 'action' => 'update'],
261
            ['table' => 'chat_video', 'field' => 'to_user', 'action' => 'update'],
262
            ['table' => 'course_rel_user', 'field' => 'user_id', 'action' => 'delete'],
263
            ['table' => 'course_rel_user_catalogue', 'field' => 'user_id', 'action' => 'delete'],
264
            ['table' => 'course_request', 'field' => 'user_id', 'action' => 'update'],
265
            ['table' => 'c_attendance_result', 'field' => 'user_id', 'action' => 'delete'],
266
            ['table' => 'c_attendance_result_comment', 'field' => 'user_id', 'action' => 'update'],
267
            ['table' => 'c_attendance_sheet', 'field' => 'user_id', 'action' => 'delete'],
268
            ['table' => 'c_attendance_sheet_log', 'field' => 'lastedit_user_id', 'action' => 'delete'],
269
            ['table' => 'c_chat_connected', 'field' => 'user_id', 'action' => 'delete'],
270
            ['table' => 'c_dropbox_category', 'field' => 'user_id', 'action' => 'update'],
271
            ['table' => 'c_dropbox_feedback', 'field' => 'author_user_id', 'action' => 'update'],
272
            ['table' => 'c_dropbox_person', 'field' => 'user_id', 'action' => 'update'],
273
            ['table' => 'c_dropbox_post', 'field' => 'dest_user_id', 'action' => 'update'],
274
            ['table' => 'c_forum_mailcue', 'field' => 'user_id', 'action' => 'delete'],
275
            ['table' => 'c_forum_notification', 'field' => 'user_id', 'action' => 'delete'],
276
            ['table' => 'c_forum_post', 'field' => 'poster_id', 'action' => 'update'],
277
            ['table' => 'c_forum_thread', 'field' => 'thread_poster_id', 'action' => 'update'],
278
            ['table' => 'c_forum_thread_qualify', 'field' => 'user_id', 'action' => 'update'],
279
            ['table' => 'c_forum_thread_qualify_log', 'field' => 'user_id', 'action' => 'update'],
280
            ['table' => 'c_group_rel_tutor', 'field' => 'user_id', 'action' => 'update'],
281
            ['table' => 'c_group_rel_user', 'field' => 'user_id', 'action' => 'update'],
282
            ['table' => 'c_lp_category_rel_user', 'field' => 'user_id', 'action' => 'delete'],
283
            ['table' => 'c_lp_rel_user', 'field' => 'user_id', 'action' => 'delete'],
284
            ['table' => 'c_lp_view', 'field' => 'user_id', 'action' => 'delete'],
285
            ['table' => 'c_student_publication_comment', 'field' => 'user_id', 'action' => 'delete'],
286
            ['table' => 'c_student_publication_rel_user', 'field' => 'user_id', 'action' => 'delete'],
287
            ['table' => 'c_survey_invitation', 'field' => 'user_id', 'action' => 'update'],
288
            ['table' => 'c_wiki', 'field' => 'user_id', 'action' => 'update'],
289
            ['table' => 'c_wiki_mailcue', 'field' => 'user_id', 'action' => 'delete'],
290
            ['table' => 'extra_field_saved_search', 'field' => 'user_id', 'action' => 'delete'],
291
            ['table' => 'gradebook_category', 'field' => 'user_id', 'action' => 'update'],
292
            ['table' => 'gradebook_certificate', 'field' => 'user_id', 'action' => 'delete'],
293
            ['table' => 'gradebook_comment', 'field' => 'user_id', 'action' => 'update'],
294
            ['table' => 'gradebook_linkeval_log', 'field' => 'user_id_log', 'action' => 'delete'],
295
            ['table' => 'gradebook_result', 'field' => 'user_id', 'action' => 'delete'],
296
            ['table' => 'gradebook_result_log', 'field' => 'user_id', 'action' => 'delete'],
297
            ['table' => 'gradebook_score_log', 'field' => 'user_id', 'action' => 'delete'],
298
            ['table' => 'message', 'field' => 'user_sender_id', 'action' => 'update'],
299
            ['table' => 'message_rel_user', 'field' => 'user_id', 'action' => 'delete'],
300
            ['table' => 'message_tag', 'field' => 'user_id', 'action' => 'delete'],
301
            ['table' => 'notification', 'field' => 'dest_user_id', 'action' => 'delete'],
302
            ['table' => 'page_category', 'field' => 'creator_id', 'action' => 'update'],
303
            ['table' => 'portfolio', 'field' => 'user_id', 'action' => 'update'],
304
            ['table' => 'portfolio_category', 'field' => 'user_id', 'action' => 'update'],
305
            ['table' => 'portfolio_comment', 'field' => 'author_id', 'action' => 'update'],
306
            ['table' => 'resource_comment', 'field' => 'author_id', 'action' => 'update'],
307
            ['table' => 'sequence_value', 'field' => 'user_id', 'action' => 'update'],
308
            ['table' => 'session_rel_course_rel_user', 'field' => 'user_id', 'action' => 'delete'],
309
            ['table' => 'session_rel_user', 'field' => 'user_id', 'action' => 'delete'],
310
            ['table' => 'skill_rel_item_rel_user', 'field' => 'user_id', 'action' => 'delete'],
311
            ['table' => 'skill_rel_user', 'field' => 'user_id', 'action' => 'delete'],
312
            ['table' => 'skill_rel_user_comment', 'field' => 'feedback_giver_id', 'action' => 'delete'],
313
            ['table' => 'social_post', 'field' => 'sender_id', 'action' => 'update'],
314
            ['table' => 'social_post', 'field' => 'user_receiver_id', 'action' => 'update'],
315
            ['table' => 'social_post_attachments', 'field' => 'sys_insert_user_id', 'action' => 'update'],
316
            ['table' => 'social_post_attachments', 'field' => 'sys_lastedit_user_id', 'action' => 'update'],
317
            ['table' => 'social_post_feedback', 'field' => 'user_id', 'action' => 'update'],
318
            ['table' => 'templates', 'field' => 'user_id', 'action' => 'update'],
319
            ['table' => 'ticket_assigned_log', 'field' => 'user_id', 'action' => 'update'],
320
            ['table' => 'ticket_assigned_log', 'field' => 'sys_insert_user_id', 'action' => 'update'],
321
            ['table' => 'ticket_category', 'field' => 'sys_insert_user_id', 'action' => 'update'],
322
            ['table' => 'ticket_category', 'field' => 'sys_lastedit_user_id', 'action' => 'update'],
323
            ['table' => 'ticket_category_rel_user', 'field' => 'user_id', 'action' => 'delete'],
324
            ['table' => 'ticket_message', 'field' => 'sys_insert_user_id', 'action' => 'update'],
325
            ['table' => 'ticket_message', 'field' => 'sys_lastedit_user_id', 'action' => 'update'],
326
            ['table' => 'ticket_message_attachments', 'field' => 'sys_insert_user_id', 'action' => 'update'],
327
            ['table' => 'ticket_message_attachments', 'field' => 'sys_lastedit_user_id', 'action' => 'update'],
328
            ['table' => 'ticket_priority', 'field' => 'sys_insert_user_id', 'action' => 'update'],
329
            ['table' => 'ticket_priority', 'field' => 'sys_lastedit_user_id', 'action' => 'update'],
330
            ['table' => 'ticket_project', 'field' => 'sys_insert_user_id', 'action' => 'update'],
331
            ['table' => 'ticket_project', 'field' => 'sys_lastedit_user_id', 'action' => 'update'],
332
            ['table' => 'track_e_access', 'field' => 'access_user_id', 'action' => 'delete'],
333
            ['table' => 'track_e_access_complete', 'field' => 'user_id', 'action' => 'delete'],
334
            ['table' => 'track_e_attempt', 'field' => 'user_id', 'action' => 'delete'],
335
            ['table' => 'track_e_course_access', 'field' => 'user_id', 'action' => 'delete'],
336
            ['table' => 'track_e_default', 'field' => 'default_user_id', 'action' => 'update'],
337
            ['table' => 'track_e_downloads', 'field' => 'down_user_id', 'action' => 'delete'],
338
            ['table' => 'track_e_exercises', 'field' => 'exe_user_id', 'action' => 'delete'],
339
            ['table' => 'track_e_exercise_confirmation', 'field' => 'user_id', 'action' => 'delete'],
340
            ['table' => 'track_e_hotpotatoes', 'field' => 'exe_user_id', 'action' => 'delete'],
341
            ['table' => 'track_e_hotspot', 'field' => 'hotspot_user_id', 'action' => 'delete'],
342
            ['table' => 'track_e_lastaccess', 'field' => 'access_user_id', 'action' => 'delete'],
343
            ['table' => 'track_e_links', 'field' => 'links_user_id', 'action' => 'delete'],
344
            ['table' => 'track_e_login', 'field' => 'login_user_id', 'action' => 'delete'],
345
            ['table' => 'track_e_online', 'field' => 'login_user_id', 'action' => 'delete'],
346
            ['table' => 'track_e_uploads', 'field' => 'upload_user_id', 'action' => 'delete'],
347
            ['table' => 'usergroup_rel_user', 'field' => 'user_id', 'action' => 'update'],
348
            ['table' => 'user_rel_tag', 'field' => 'user_id', 'action' => 'delete'],
349
            ['table' => 'user_rel_user', 'field' => 'user_id', 'action' => 'delete'],
350
        ];
351
    }
352
353
    /**
354
     * Deletes a user's messages and their attachments, updates the message content,
355
     * and detaches the user as the sender.
356
     */
357
    public function deleteUserMessagesAndAttachments(User $user): void
358
    {
359
        $em = $this->getEntityManager();
360
        $connection = $em->getConnection();
361
362
        $currentDate = (new Datetime())->format('Y-m-d H:i:s');
363
        $updatedContent = \sprintf(
364
            $this->translator->trans('This message was deleted when the user was removed from the platform on %s'),
365
            $currentDate
366
        );
367
368
        $connection->executeStatement(
369
            'UPDATE message m
370
         SET m.content = :content, m.user_sender_id = NULL
371
         WHERE m.user_sender_id = :userId',
372
            [
373
                'content' => $updatedContent,
374
                'userId' => $user->getId(),
375
            ]
376
        );
377
378
        $connection->executeStatement(
379
            'DELETE ma
380
         FROM message_attachment ma
381
         INNER JOIN message m ON ma.message_id = m.id
382
         WHERE m.user_sender_id IS NULL',
383
            [
384
                'userId' => $user->getId(),
385
            ]
386
        );
387
388
        $em->clear();
389
    }
390
391
    public function getFallbackUser(): ?User
392
    {
393
        return $this->findOneBy(['status' => User::ROLE_FALLBACK], ['id' => 'ASC']);
394
    }
395
396
    public function addUserToResourceNode(int $userId, int $creatorId): ResourceNode
397
    {
398
        /** @var User $user */
399
        $user = $this->find($userId);
400
        $creator = $this->find($creatorId);
401
402
        $resourceNode = (new ResourceNode())
403
            ->setTitle($user->getUsername())
404
            ->setCreator($creator)
405
            ->setResourceType($this->getResourceType())
406
            // ->setParent($resourceNode)
407
        ;
408
409
        $user->setResourceNode($resourceNode);
410
411
        $this->getEntityManager()->persist($resourceNode);
412
        $this->getEntityManager()->persist($user);
413
414
        return $resourceNode;
415
    }
416
417
    public function addRoleListQueryBuilder(array $roles, ?QueryBuilder $qb = null): QueryBuilder
418
    {
419
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
420
        if (!empty($roles)) {
421
            $orX = $qb->expr()->orX();
422
            foreach ($roles as $role) {
423
                $orX->add($qb->expr()->like('u.roles', ':'.$role));
424
                $qb->setParameter($role, '%'.$role.'%');
425
            }
426
            $qb->andWhere($orX);
427
        }
428
429
        return $qb;
430
    }
431
432
    public function findByUsername(string $username): ?User
433
    {
434
        $user = $this->findOneBy([
435
            'username' => $username,
436
        ]);
437
438
        if (null === $user) {
439
            throw new UserNotFoundException(\sprintf("User with id '%s' not found.", $username));
440
        }
441
442
        return $user;
443
    }
444
445
    /**
446
     * Get a filtered list of user by role and (optionally) access url.
447
     *
448
     * @param string $keyword     The query to filter
449
     * @param int    $accessUrlId The access URL ID
450
     *
451
     * @return User[]
452
     */
453
    public function findByRole(string $role, string $keyword, int $accessUrlId = 0)
454
    {
455
        $qb = $this->createQueryBuilder('u');
456
457
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
458
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
459
        $this->addRoleQueryBuilder($role, $qb);
460
        $this->addSearchByKeywordQueryBuilder($keyword, $qb);
461
462
        return $qb->getQuery()->getResult();
463
    }
464
465
    public function findByRoleList(array $roleList, string $keyword, int $accessUrlId = 0)
466
    {
467
        $qb = $this->createQueryBuilder('u');
468
469
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
470
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
471
        $this->addRoleListQueryBuilder($roleList, $qb);
472
        $this->addSearchByKeywordQueryBuilder($keyword, $qb);
473
474
        return $qb->getQuery()->getResult();
475
    }
476
477
    /**
478
     * Get the coaches for a course within a session.
479
     *
480
     * @return Collection|array
481
     */
482
    public function getCoachesForSessionCourse(Session $session, Course $course)
483
    {
484
        $qb = $this->createQueryBuilder('u');
485
486
        $qb->select('u')
487
            ->innerJoin(
488
                'ChamiloCoreBundle:SessionRelCourseRelUser',
489
                'scu',
490
                Join::WITH,
491
                'scu.user = u'
492
            )
493
            ->where(
494
                $qb->expr()->andX(
495
                    $qb->expr()->eq('scu.session', $session->getId()),
496
                    $qb->expr()->eq('scu.course', $course->getId()),
497
                    $qb->expr()->eq('scu.status', Session::COURSE_COACH)
498
                )
499
            )
500
        ;
501
502
        return $qb->getQuery()->getResult();
503
    }
504
505
    /**
506
     * Get the sessions admins for a user.
507
     *
508
     * @return array
509
     */
510
    public function getSessionAdmins(User $user)
511
    {
512
        $qb = $this->createQueryBuilder('u');
513
        $qb
514
            ->distinct()
515
            ->innerJoin(
516
                'ChamiloCoreBundle:SessionRelUser',
517
                'su',
518
                Join::WITH,
519
                'u = su.user'
520
            )
521
            ->innerJoin(
522
                'ChamiloCoreBundle:SessionRelCourseRelUser',
523
                'scu',
524
                Join::WITH,
525
                'su.session = scu.session'
526
            )
527
            ->where(
528
                $qb->expr()->eq('scu.user', $user->getId())
529
            )
530
            ->andWhere(
531
                $qb->expr()->eq('su.relationType', Session::DRH)
532
            )
533
        ;
534
535
        return $qb->getQuery()->getResult();
536
    }
537
538
    /**
539
     * Get number of users in URL.
540
     *
541
     * @return int
542
     */
543
    public function getCountUsersByUrl(AccessUrl $url)
544
    {
545
        return $this->createQueryBuilder('u')
546
            ->select('COUNT(u)')
547
            ->innerJoin('u.portals', 'p')
548
            ->where('p.url = :url')
549
            ->setParameters([
550
                'url' => $url,
551
            ])
552
            ->getQuery()
553
            ->getSingleScalarResult()
554
        ;
555
    }
556
557
    /**
558
     * Get number of users in URL.
559
     *
560
     * @return int
561
     */
562
    public function getCountTeachersByUrl(AccessUrl $url)
563
    {
564
        $qb = $this->createQueryBuilder('u');
565
566
        $qb
567
            ->select('COUNT(u)')
568
            ->innerJoin('u.portals', 'p')
569
            ->where('p.url = :url')
570
            ->setParameters([
571
                'url' => $url,
572
            ])
573
        ;
574
575
        $this->addRoleListQueryBuilder(['ROLE_TEACHER'], $qb);
576
577
        return (int) $qb->getQuery()->getSingleScalarResult();
578
    }
579
580
    /**
581
     * Find potential users to send a message.
582
     *
583
     * @todo remove  api_is_platform_admin
584
     *
585
     * @param int    $currentUserId The current user ID
586
     * @param string $searchFilter  Optional. The search text to filter the user list
587
     * @param int    $limit         Optional. Sets the maximum number of results to retrieve
588
     *
589
     * @return User[]
590
     */
591
    public function findUsersToSendMessage(int $currentUserId, ?string $searchFilter = null, int $limit = 10)
592
    {
593
        $allowSendMessageToAllUsers = api_get_setting('allow_send_message_to_all_platform_users');
594
        $accessUrlId = api_get_multiple_access_url() ? api_get_current_access_url_id() : 1;
595
596
        $messageTool = 'true' === api_get_setting('allow_message_tool');
597
        if (!$messageTool) {
598
            return [];
599
        }
600
601
        $qb = $this->createQueryBuilder('u');
602
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
603
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
604
605
        $dql = null;
606
        if ('true' === api_get_setting('allow_social_tool')) {
607
            // All users
608
            if ('true' === $allowSendMessageToAllUsers || api_is_platform_admin()) {
609
                $this->addNotCurrentUserQueryBuilder($currentUserId, $qb);
610
            /*$dql = "SELECT DISTINCT U
611
                    FROM ChamiloCoreBundle:User U
612
                    LEFT JOIN ChamiloCoreBundle:AccessUrlRelUser R
613
                    WITH U = R.user
614
                    WHERE
615
                        U.active = 1 AND
616
                        U.status != 6  AND
617
                        U.id != {$currentUserId} AND
618
                        R.url = {$accessUrlId}";*/
619
            } else {
620
                $this->addOnlyMyFriendsQueryBuilder($currentUserId, $qb);
621
                /*$dql = 'SELECT DISTINCT U
622
                        FROM ChamiloCoreBundle:AccessUrlRelUser R, ChamiloCoreBundle:UserRelUser UF
623
                        INNER JOIN ChamiloCoreBundle:User AS U
624
                        WITH UF.friendUserId = U
625
                        WHERE
626
                            U.active = 1 AND
627
                            U.status != 6 AND
628
                            UF.relationType NOT IN('.USER_RELATION_TYPE_DELETED.', '.USER_RELATION_TYPE_RRHH.") AND
629
                            UF.user = {$currentUserId} AND
630
                            UF.friendUserId != {$currentUserId} AND
631
                            U = R.user AND
632
                            R.url = {$accessUrlId}";*/
633
            }
634
        } else {
635
            if ('true' === $allowSendMessageToAllUsers) {
636
                $this->addNotCurrentUserQueryBuilder($currentUserId, $qb);
637
            } else {
638
                return [];
639
            }
640
641
            /*else {
642
                $time_limit = (int) api_get_setting('time_limit_whosonline');
643
                $online_time = time() - ($time_limit * 60);
644
                $limit_date = api_get_utc_datetime($online_time);
645
                $dql = "SELECT DISTINCT U
646
                        FROM ChamiloCoreBundle:User U
647
                        INNER JOIN ChamiloCoreBundle:TrackEOnline T
648
                        WITH U.id = T.loginUserId
649
                        WHERE
650
                          U.active = 1 AND
651
                          T.loginDate >= '".$limit_date."'";
652
            }*/
653
        }
654
655
        if (!empty($searchFilter)) {
656
            $this->addSearchByKeywordQueryBuilder($searchFilter, $qb);
657
        }
658
659
        return $qb->getQuery()->getResult();
660
    }
661
662
    /**
663
     * Get the list of HRM who have assigned this user.
664
     *
665
     * @return User[]
666
     */
667
    public function getAssignedHrmUserList(int $userId, int $urlId)
668
    {
669
        $qb = $this->createQueryBuilder('u');
670
        $this->addAccessUrlQueryBuilder($urlId, $qb);
671
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
672
        $this->addUserRelUserQueryBuilder($userId, UserRelUser::USER_RELATION_TYPE_RRHH, $qb);
673
674
        return $qb->getQuery()->getResult();
675
    }
676
677
    /**
678
     * Get the last login from the track_e_login table.
679
     * This might be different from user.last_login in the case of legacy users
680
     * as user.last_login was only implemented in 1.10 version with a default
681
     * value of NULL (not the last record from track_e_login).
682
     *
683
     * @return null|TrackELogin
684
     */
685
    public function getLastLogin(User $user)
686
    {
687
        $qb = $this->createQueryBuilder('u');
688
689
        return $qb
690
            ->select('l')
691
            ->innerJoin('u.logins', 'l')
692
            ->where(
693
                $qb->expr()->eq('l.user', $user)
694
            )
695
            ->setMaxResults(1)
696
            ->orderBy('u.loginDate', Criteria::DESC)
697
            ->getQuery()
698
            ->getOneOrNullResult()
699
        ;
700
    }
701
702
    public function addAccessUrlQueryBuilder(int $accessUrlId, ?QueryBuilder $qb = null): QueryBuilder
703
    {
704
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
705
        $qb
706
            ->innerJoin('u.portals', 'p')
707
            ->andWhere('p.url = :url')
708
            ->setParameter('url', $accessUrlId, Types::INTEGER)
709
        ;
710
711
        return $qb;
712
    }
713
714
    public function addActiveAndNotAnonUserQueryBuilder(?QueryBuilder $qb = null): QueryBuilder
715
    {
716
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
717
        $qb
718
            ->andWhere('u.active = 1')
719
            ->andWhere('u.status <> :status')
720
            ->setParameter('status', User::ANONYMOUS, Types::INTEGER)
721
        ;
722
723
        return $qb;
724
    }
725
726
    public function addExpirationDateQueryBuilder(?QueryBuilder $qb = null): QueryBuilder
727
    {
728
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
729
        $qb
730
            ->andWhere('u.expirationDate IS NULL OR u.expirationDate > :now')
731
            ->setParameter('now', new Datetime(), Types::DATETIME_MUTABLE)
732
        ;
733
734
        return $qb;
735
    }
736
737
    private function addRoleQueryBuilder(string $role, ?QueryBuilder $qb = null): QueryBuilder
738
    {
739
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
740
        $qb
741
            ->andWhere('u.roles LIKE :roles')
742
            ->setParameter('roles', '%"'.$role.'"%', Types::STRING)
743
        ;
744
745
        return $qb;
746
    }
747
748
    private function addSearchByKeywordQueryBuilder(string $keyword, ?QueryBuilder $qb = null): QueryBuilder
749
    {
750
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
751
        $qb
752
            ->andWhere('
753
                u.firstname LIKE :keyword OR
754
                u.lastname LIKE :keyword OR
755
                u.email LIKE :keyword OR
756
                u.username LIKE :keyword
757
            ')
758
            ->setParameter('keyword', "%$keyword%", Types::STRING)
759
            ->orderBy('u.firstname', Criteria::ASC)
760
        ;
761
762
        return $qb;
763
    }
764
765
    private function addUserRelUserQueryBuilder(int $userId, int $relationType, ?QueryBuilder $qb = null): QueryBuilder
766
    {
767
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
768
        $qb->leftJoin('u.friends', 'relations');
769
        $qb
770
            ->andWhere('relations.relationType = :relationType')
771
            ->andWhere('relations.user = :userRelation AND relations.friend <> :userRelation')
772
            ->setParameter('relationType', $relationType)
773
            ->setParameter('userRelation', $userId)
774
        ;
775
776
        return $qb;
777
    }
778
779
    private function addOnlyMyFriendsQueryBuilder(int $userId, ?QueryBuilder $qb = null): QueryBuilder
780
    {
781
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
782
        $qb
783
            ->leftJoin('u.friends', 'relations')
784
            ->andWhere(
785
                $qb->expr()->notIn(
786
                    'relations.relationType',
787
                    [UserRelUser::USER_RELATION_TYPE_DELETED, UserRelUser::USER_RELATION_TYPE_RRHH]
788
                )
789
            )
790
            ->andWhere('relations.user = :user AND relations.friend <> :user')
791
            ->setParameter('user', $userId, Types::INTEGER)
792
        ;
793
794
        return $qb;
795
    }
796
797
    private function addNotCurrentUserQueryBuilder(int $userId, ?QueryBuilder $qb = null): QueryBuilder
798
    {
799
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
800
        $qb
801
            ->andWhere('u.id <> :id')
802
            ->setParameter('id', $userId, Types::INTEGER)
803
        ;
804
805
        return $qb;
806
    }
807
808
    public function getFriendsNotInGroup(int $userId, int $groupId)
809
    {
810
        $entityManager = $this->getEntityManager();
811
812
        $subQueryBuilder = $entityManager->createQueryBuilder();
813
        $subQuery = $subQueryBuilder
814
            ->select('IDENTITY(ugr.user)')
815
            ->from(UsergroupRelUser::class, 'ugr')
816
            ->where('ugr.usergroup = :subGroupId')
817
            ->andWhere('ugr.relationType IN (:subRelationTypes)')
818
            ->getDQL()
819
        ;
820
821
        $queryBuilder = $entityManager->createQueryBuilder();
822
        $query = $queryBuilder
823
            ->select('u')
824
            ->from(User::class, 'u')
825
            ->leftJoin('u.friendsWithMe', 'uruf')
826
            ->leftJoin('u.friends', 'urut')
827
            ->where('uruf.friend = :userId OR urut.user = :userId')
828
            ->andWhere($queryBuilder->expr()->notIn('u.id', $subQuery))
829
            ->setParameter('userId', $userId)
830
            ->setParameter('subGroupId', $groupId)
831
            ->setParameter('subRelationTypes', [Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION])
832
            ->getQuery()
833
        ;
834
835
        return $query->getResult();
836
    }
837
838
    public function getExtraUserData(int $userId, bool $prefix = false, bool $allVisibility = true, bool $splitMultiple = false, ?int $fieldFilter = null): array
839
    {
840
        $qb = $this->getEntityManager()->createQueryBuilder();
841
842
        // Start building the query
843
        $qb->select('ef.id', 'ef.variable as fvar', 'ef.valueType as type', 'efv.fieldValue as fval', 'ef.defaultValue as fval_df')
844
            ->from(ExtraField::class, 'ef')
845
            ->leftJoin(ExtraFieldValues::class, 'efv', Join::WITH, 'efv.field = ef.id AND efv.itemId = :userId')
846
            ->where('ef.itemType = :itemType')
847
            ->setParameter('userId', $userId)
848
            ->setParameter('itemType', ExtraField::USER_FIELD_TYPE)
849
        ;
850
851
        // Apply visibility filters
852
        if (!$allVisibility) {
853
            $qb->andWhere('ef.visibleToSelf = true');
854
        }
855
856
        // Apply field filter if provided
857
        if (null !== $fieldFilter) {
858
            $qb->andWhere('ef.id = :fieldFilter')
859
                ->setParameter('fieldFilter', $fieldFilter)
860
            ;
861
        }
862
863
        // Order by field order
864
        $qb->orderBy('ef.fieldOrder', 'ASC');
865
866
        // Execute the query
867
        $results = $qb->getQuery()->getResult();
868
869
        // Process results
870
        $extraData = [];
871
        foreach ($results as $row) {
872
            $value = $row['fval'] ?? $row['fval_df'];
873
874
            // Handle multiple values if necessary
875
            if ($splitMultiple && \in_array($row['type'], [ExtraField::USER_FIELD_TYPE_SELECT_MULTIPLE], true)) {
876
                $value = explode(';', $value);
877
            }
878
879
            // Handle prefix if needed
880
            $key = $prefix ? 'extra_'.$row['fvar'] : $row['fvar'];
881
882
            // Special handling for certain field types
883
            if (ExtraField::USER_FIELD_TYPE_TAG == $row['type']) {
884
                // Implement your logic to handle tags
885
            } elseif (ExtraField::USER_FIELD_TYPE_RADIO == $row['type'] && $prefix) {
886
                $extraData[$key][$key] = $value;
887
            } else {
888
                $extraData[$key] = $value;
889
            }
890
        }
891
892
        return $extraData;
893
    }
894
895
    public function getExtraUserDataByField(int $userId, string $fieldVariable, bool $allVisibility = true): array
896
    {
897
        $qb = $this->getEntityManager()->createQueryBuilder();
898
899
        $qb->select('e.id, e.variable, e.valueType, v.fieldValue')
900
            ->from(ExtraFieldValues::class, 'v')
901
            ->innerJoin('v.field', 'e')
902
            ->where('v.itemId = :userId')
903
            ->andWhere('e.variable = :fieldVariable')
904
            ->andWhere('e.itemType = :itemType')
905
            ->setParameters([
906
                'userId' => $userId,
907
                'fieldVariable' => $fieldVariable,
908
                'itemType' => ExtraField::USER_FIELD_TYPE,
909
            ])
910
        ;
911
912
        if (!$allVisibility) {
913
            $qb->andWhere('e.visibleToSelf = true');
914
        }
915
916
        $qb->orderBy('e.fieldOrder', 'ASC');
917
918
        $result = $qb->getQuery()->getResult();
919
920
        $extraData = [];
921
        foreach ($result as $row) {
922
            $value = $row['fieldValue'];
923
            if (ExtraField::USER_FIELD_TYPE_SELECT_MULTIPLE == $row['valueType']) {
924
                $value = explode(';', $row['fieldValue']);
925
            }
926
927
            $extraData[$row['variable']] = $value;
928
        }
929
930
        return $extraData;
931
    }
932
933
    public function searchUsersByTags(
934
        string $tag,
935
        ?int $excludeUserId = null,
936
        int $fieldId = 0,
937
        int $from = 0,
938
        int $number_of_items = 10,
939
        bool $getCount = false
940
    ): array {
941
        $qb = $this->createQueryBuilder('u');
942
943
        if ($getCount) {
944
            $qb->select('COUNT(DISTINCT u.id)');
945
        } else {
946
            $qb->select('DISTINCT u.id, u.username, u.firstname, u.lastname, u.email, u.pictureUri, u.status');
947
        }
948
949
        $qb->innerJoin('u.portals', 'urlRelUser')
950
            ->leftJoin(UserRelTag::class, 'uv', 'WITH', 'u = uv.user')
951
            ->leftJoin(Tag::class, 'ut', 'WITH', 'uv.tag = ut')
952
        ;
953
954
        if (0 !== $fieldId) {
955
            $qb->andWhere('ut.field = :fieldId')
956
                ->setParameter('fieldId', $fieldId)
957
            ;
958
        }
959
960
        if (null !== $excludeUserId) {
961
            $qb->andWhere('u.id != :excludeUserId')
962
                ->setParameter('excludeUserId', $excludeUserId)
963
            ;
964
        }
965
966
        $qb->andWhere(
967
            $qb->expr()->orX(
968
                $qb->expr()->like('ut.tag', ':tag'),
969
                $qb->expr()->like('u.firstname', ':likeTag'),
970
                $qb->expr()->like('u.lastname', ':likeTag'),
971
                $qb->expr()->like('u.username', ':likeTag'),
972
                $qb->expr()->like(
973
                    $qb->expr()->concat('u.firstname', $qb->expr()->literal(' '), 'u.lastname'),
974
                    ':likeTag'
975
                ),
976
                $qb->expr()->like(
977
                    $qb->expr()->concat('u.lastname', $qb->expr()->literal(' '), 'u.firstname'),
978
                    ':likeTag'
979
                )
980
            )
981
        )
982
            ->setParameter('tag', $tag.'%')
983
            ->setParameter('likeTag', '%'.$tag.'%')
984
        ;
985
986
        // Only active users and not anonymous
987
        $qb->andWhere('u.active = :active')
988
            ->andWhere('u.status != :anonymous')
989
            ->setParameter('active', true)
990
            ->setParameter('anonymous', 6)
991
        ;
992
993
        if (!$getCount) {
994
            $qb->orderBy('u.username')
995
                ->setFirstResult($from)
996
                ->setMaxResults($number_of_items)
997
            ;
998
        }
999
1000
        return $getCount ? $qb->getQuery()->getSingleScalarResult() : $qb->getQuery()->getResult();
1001
    }
1002
1003
    public function getUserRelationWithType(int $userId, int $friendId): ?array
1004
    {
1005
        $qb = $this->createQueryBuilder('u');
1006
        $qb->select('u.id AS userId', 'u.username AS userName', 'ur.relationType', 'f.id AS friendId', 'f.username AS friendName')
1007
            ->innerJoin('u.friends', 'ur')
1008
            ->innerJoin('ur.friend', 'f')
1009
            ->where('u.id = :userId AND f.id = :friendId')
1010
            ->setParameter('userId', $userId)
1011
            ->setParameter('friendId', $friendId)
1012
            ->setMaxResults(1)
1013
        ;
1014
1015
        return $qb->getQuery()->getOneOrNullResult();
1016
    }
1017
1018
    public function relateUsers(User $user1, User $user2, int $relationType): void
1019
    {
1020
        $em = $this->getEntityManager();
1021
1022
        $existingRelation = $em->getRepository(UserRelUser::class)->findOneBy([
1023
            'user' => $user1,
1024
            'friend' => $user2,
1025
        ]);
1026
1027
        if (!$existingRelation) {
1028
            $newRelation = new UserRelUser();
1029
            $newRelation->setUser($user1);
1030
            $newRelation->setFriend($user2);
1031
            $newRelation->setRelationType($relationType);
1032
            $em->persist($newRelation);
1033
        } else {
1034
            $existingRelation->setRelationType($relationType);
1035
        }
1036
1037
        $existingRelationInverse = $em->getRepository(UserRelUser::class)->findOneBy([
1038
            'user' => $user2,
1039
            'friend' => $user1,
1040
        ]);
1041
1042
        if (!$existingRelationInverse) {
1043
            $newRelationInverse = new UserRelUser();
1044
            $newRelationInverse->setUser($user2);
1045
            $newRelationInverse->setFriend($user1);
1046
            $newRelationInverse->setRelationType($relationType);
1047
            $em->persist($newRelationInverse);
1048
        } else {
1049
            $existingRelationInverse->setRelationType($relationType);
1050
        }
1051
1052
        $em->flush();
1053
    }
1054
1055
    public function getUserPicture(
1056
        $userId,
1057
        int $size = self::USER_IMAGE_SIZE_MEDIUM,
1058
        $addRandomId = true,
1059
    ) {
1060
        $user = $this->find($userId);
1061
        if (!$user) {
1062
            return '/img/icons/64/unknown.png';
1063
        }
1064
1065
        switch ($size) {
1066
            case self::USER_IMAGE_SIZE_SMALL:
1067
                $width = 32;
1068
1069
                break;
1070
1071
            case self::USER_IMAGE_SIZE_MEDIUM:
1072
                $width = 64;
1073
1074
                break;
1075
1076
            case self::USER_IMAGE_SIZE_BIG:
1077
                $width = 128;
1078
1079
                break;
1080
1081
            case self::USER_IMAGE_SIZE_ORIGINAL:
1082
            default:
1083
                $width = 0;
1084
1085
                break;
1086
        }
1087
1088
        $url = $this->illustrationRepository->getIllustrationUrl($user);
1089
        $params = [];
1090
        if (!empty($width)) {
1091
            $params['w'] = $width;
1092
        }
1093
1094
        if ($addRandomId) {
1095
            $params['rand'] = uniqid('u_', true);
1096
        }
1097
1098
        $paramsToString = '';
1099
        if (!empty($params)) {
1100
            $paramsToString = '?'.http_build_query($params);
1101
        }
1102
1103
        return $url.$paramsToString;
1104
    }
1105
1106
    /**
1107
     * Retrieves the list of DRH (HR) users related to a specific user and access URL.
1108
     */
1109
    public function getDrhListFromUser(int $userId, int $accessUrlId): array
1110
    {
1111
        $qb = $this->createQueryBuilder('u');
1112
1113
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
1114
1115
        $qb->select('u.id, u.username, u.firstname, u.lastname')
1116
            ->innerJoin('u.friends', 'uru', Join::WITH, 'uru.friend = u.id')
1117
            ->where('uru.user = :userId')
1118
            ->andWhere('uru.relationType = :relationType')
1119
            ->setParameter('userId', $userId)
1120
            ->setParameter('relationType', UserRelUser::USER_RELATION_TYPE_RRHH)
1121
        ;
1122
1123
        $qb->orderBy('u.lastname', 'ASC')
1124
            ->addOrderBy('u.firstname', 'ASC')
1125
        ;
1126
1127
        return $qb->getQuery()->getResult();
1128
    }
1129
1130
    public function findUsersByContext(int $courseId, ?int $sessionId = null, ?int $groupId = null): array
1131
    {
1132
        $course = $this->_em->getRepository(Course::class)->find($courseId);
1133
        if (!$course) {
1134
            throw new \InvalidArgumentException('Course not found.');
1135
        }
1136
1137
        if ($sessionId !== null) {
1138
            $session = $this->_em->getRepository(Session::class)->find($sessionId);
1139
            if (!$session) {
1140
                throw new \InvalidArgumentException('Session not found.');
1141
            }
1142
1143
            $list = $session->getSessionRelCourseRelUsersByStatus($course, Session::STUDENT);
1144
            $users = [];
1145
1146
            if ($list) {
1147
                foreach ($list as $sessionCourseUser) {
1148
                    $users[$sessionCourseUser->getUser()->getId()] = $sessionCourseUser->getUser();
1149
                }
1150
            }
1151
1152
            return array_values($users);
1153
        }
1154
1155
        if ($groupId !== null) {
1156
            $qb = $this->_em->createQueryBuilder();
1157
            $qb->select('u')
1158
                ->from(CGroupRelUser::class, 'cgru')
1159
                ->innerJoin('cgru.user', 'u')
1160
                ->where('cgru.cId = :courseId')
1161
                ->andWhere('cgru.group = :groupId')
1162
                ->setParameters([
1163
                    'courseId' => $courseId,
1164
                    'groupId' => $groupId,
1165
                ])
1166
                ->orderBy('u.lastname', 'ASC')
1167
                ->addOrderBy('u.firstname', 'ASC');
1168
1169
            return $qb->getQuery()->getResult();
1170
        }
1171
1172
        $queryBuilder = $this->_em->getRepository(Course::class)->getSubscribedStudents($course);
1173
        return $queryBuilder->getQuery()->getResult();
1174
    }
1175
}
1176