Completed
Push — master ( 00dc55...ee9767 )
by
unknown
01:36 queued 48s
created

UserRepository::updatePassword()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Repository\Node;
8
9
use Chamilo\CoreBundle\Entity\AccessUrl;
10
use Chamilo\CoreBundle\Entity\Course;
11
use Chamilo\CoreBundle\Entity\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
38
use const MB_CASE_LOWER;
39
40
class UserRepository extends ResourceRepository implements PasswordUpgraderInterface
41
{
42
    protected ?UserPasswordHasherInterface $hasher = null;
43
44
    public const USER_IMAGE_SIZE_SMALL = 1;
45
    public const USER_IMAGE_SIZE_MEDIUM = 2;
46
    public const USER_IMAGE_SIZE_BIG = 3;
47
    public const USER_IMAGE_SIZE_ORIGINAL = 4;
48
49
    public function __construct(
50
        ManagerRegistry $registry,
51
        private readonly IllustrationRepository $illustrationRepository
52
    ) {
53
        parent::__construct($registry, User::class);
54
    }
55
56
    public function loadUserByIdentifier(string $identifier): ?User
57
    {
58
        return $this->findOneBy([
59
            'username' => $identifier,
60
        ]);
61
    }
62
63
    public function setHasher(UserPasswordHasherInterface $hasher): void
64
    {
65
        $this->hasher = $hasher;
66
    }
67
68
    public function createUser(): User
69
    {
70
        return new User();
71
    }
72
73
    public function updateUser(User $user, bool $andFlush = true): void
74
    {
75
        $this->updateCanonicalFields($user);
76
        $this->updatePassword($user);
77
        $this->getEntityManager()->persist($user);
78
        if ($andFlush) {
79
            $this->getEntityManager()->flush();
80
        }
81
    }
82
83
    public function canonicalize(string $string): string
84
    {
85
        $encoding = mb_detect_encoding($string, mb_detect_order(), true);
86
87
        return $encoding
88
            ? mb_convert_case($string, MB_CASE_LOWER, $encoding)
89
            : mb_convert_case($string, MB_CASE_LOWER);
90
    }
91
92
    public function updateCanonicalFields(User $user): void
93
    {
94
        $user->setUsernameCanonical($this->canonicalize($user->getUsername()));
95
        $user->setEmailCanonical($this->canonicalize($user->getEmail()));
96
    }
97
98
    public function updatePassword(User $user): void
99
    {
100
        $password = (string) $user->getPlainPassword();
101
        if ('' !== $password) {
102
            $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

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