Passed
Push — dependabot/npm_and_yarn/nanoid... ( aaf2c9...c4aa90 )
by
unknown
14:37 queued 06:22
created

UserRepository::deleteUser()   A

Complexity

Conditions 4
Paths 16

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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

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