Passed
Push — master ( 33b068...42cb13 )
by
unknown
15:05 queued 07:02
created

addActiveAndNotAnonUserQueryBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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