Passed
Push — master ( 1c618d...c1c6b0 )
by Yannick
10:36 queued 02:52
created

UserRepository::isPasswordValid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
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
        $em = $this->getEntityManager();
148
        $em->getConnection()->beginTransaction();
149
150
        try {
151
            if ($destroy) {
152
                $fallbackUser = $this->getFallbackUser();
153
154
                if ($fallbackUser) {
155
                    $this->reassignUserResourcesToFallback($user, $fallbackUser);
156
                    $em->flush();
157
                }
158
159
                foreach ($user->getGroups() as $group) {
160
                    $user->removeGroup($group);
161
                }
162
163
                if ($user->getResourceNode()) {
164
                    $em->remove($user->getResourceNode());
165
                }
166
167
                $em->remove($user);
168
            } else {
169
                $user->setActive(User::SOFT_DELETED);
170
                $em->persist($user);
171
            }
172
173
            $em->flush();
174
            $em->getConnection()->commit();
175
        } catch (Exception $e) {
176
            $em->getConnection()->rollBack();
177
178
            throw $e;
179
        }
180
    }
181
182
    protected function reassignUserResourcesToFallback(User $userToDelete, User $fallbackUser): void
183
    {
184
        $em = $this->getEntityManager();
185
186
        $userResourceNodes = $em->getRepository(ResourceNode::class)->findBy(['creator' => $userToDelete]);
187
        foreach ($userResourceNodes as $resourceNode) {
188
            $resourceNode->setCreator($fallbackUser);
189
            $em->persist($resourceNode);
190
        }
191
192
        $childResourceNodes = $em->getRepository(ResourceNode::class)->findBy(['parent' => $userToDelete->getResourceNode()]);
193
        foreach ($childResourceNodes as $childNode) {
194
            $fallbackUserResourceNode = $fallbackUser->getResourceNode();
195
            if ($fallbackUserResourceNode) {
196
                $childNode->setParent($fallbackUserResourceNode);
197
            } else {
198
                $childNode->setParent(null);
199
            }
200
            $em->persist($childNode);
201
        }
202
203
        $relations = [
204
            ['bundle' => 'CoreBundle', 'entity' => 'AccessUrlRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
205
            ['bundle' => 'CoreBundle', 'entity' => 'Admin', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
206
            ['bundle' => 'CoreBundle', 'entity' => 'AttemptFeedback', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
207
            ['bundle' => 'CoreBundle', 'entity' => 'Chat', 'field' => 'toUser', 'type' => 'int', 'action' => 'convert'],
208
            ['bundle' => 'CoreBundle', 'entity' => 'ChatVideo', 'field' => 'toUser', 'type' => 'int', 'action' => 'convert'],
209
            ['bundle' => 'CoreBundle', 'entity' => 'CourseRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
210
            ['bundle' => 'CoreBundle', 'entity' => 'CourseRelUserCatalogue', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
211
            ['bundle' => 'CoreBundle', 'entity' => 'CourseRequest', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
212
            ['bundle' => 'CourseBundle', 'entity' => 'CAttendanceResult', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
213
            ['bundle' => 'CourseBundle', 'entity' => 'CAttendanceResultComment', 'field' => 'userId', 'type' => 'int', 'action' => 'convert'],
214
            ['bundle' => 'CourseBundle', 'entity' => 'CAttendanceSheet', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
215
            ['bundle' => 'CourseBundle', 'entity' => 'CAttendanceSheetLog', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
216
            ['bundle' => 'CourseBundle', 'entity' => 'CChatConnected', 'field' => 'userId', 'type' => 'int', 'action' => 'delete'],
217
            ['bundle' => 'CourseBundle', 'entity' => 'CDropboxCategory', 'field' => 'userId', 'type' => 'int', 'action' => 'convert'],
218
            ['bundle' => 'CourseBundle', 'entity' => 'CDropboxFeedback', 'field' => 'authorUserId', 'type' => 'int', 'action' => 'convert'],
219
            ['bundle' => 'CourseBundle', 'entity' => 'CDropboxPerson', 'field' => 'userId', 'type' => 'int', 'action' => 'convert'],
220
            ['bundle' => 'CourseBundle', 'entity' => 'CDropboxPost', 'field' => 'destUserId', 'type' => 'int', 'action' => 'convert'],
221
            ['bundle' => 'CourseBundle', 'entity' => 'CForumMailcue', 'field' => 'userId', 'type' => 'int', 'action' => 'delete'],
222
            ['bundle' => 'CourseBundle', 'entity' => 'CForumNotification', 'field' => 'userId', 'type' => 'int', 'action' => 'delete'],
223
            ['bundle' => 'CourseBundle', 'entity' => 'CForumPost', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
224
            ['bundle' => 'CourseBundle', 'entity' => 'CForumThread', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
225
            ['bundle' => 'CourseBundle', 'entity' => 'CForumThreadQualify', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
226
            ['bundle' => 'CourseBundle', 'entity' => 'CForumThreadQualifyLog', 'field' => 'userId', 'type' => 'int', 'action' => 'convert'],
227
            ['bundle' => 'CourseBundle', 'entity' => 'CGroupRelTutor', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
228
            ['bundle' => 'CourseBundle', 'entity' => 'CGroupRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
229
            ['bundle' => 'CourseBundle', 'entity' => 'CLpCategoryRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
230
            ['bundle' => 'CourseBundle', 'entity' => 'CLpRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
231
            ['bundle' => 'CourseBundle', 'entity' => 'CLpView', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
232
            ['bundle' => 'CourseBundle', 'entity' => 'CStudentPublicationComment', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
233
            ['bundle' => 'CourseBundle', 'entity' => 'CStudentPublicationRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
234
            ['bundle' => 'CourseBundle', 'entity' => 'CSurveyInvitation', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
235
            ['bundle' => 'CourseBundle', 'entity' => 'CWiki', 'field' => 'userId', 'type' => 'int', 'action' => 'convert'],
236
            ['bundle' => 'CourseBundle', 'entity' => 'CWikiMailcue', 'field' => 'userId', 'type' => 'int', 'action' => 'delete'],
237
            ['bundle' => 'CoreBundle', 'entity' => 'ExtraFieldSavedSearch', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
238
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookCategory', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
239
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookCertificate', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
240
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookComment', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
241
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookEvaluation', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
242
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookLink', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
243
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookLinkevalLog', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
244
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookResult', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
245
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookResultLog', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
246
            ['bundle' => 'CoreBundle', 'entity' => 'GradebookScoreLog', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
247
            ['bundle' => 'CoreBundle', 'entity' => 'Message', 'field' => 'sender', 'type' => 'object', 'action' => 'convert'],
248
            ['bundle' => 'CoreBundle', 'entity' => 'MessageFeedback', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
249
            ['bundle' => 'CoreBundle', 'entity' => 'MessageRelUser', 'field' => 'receiver', 'type' => 'object', 'action' => 'delete'],
250
            ['bundle' => 'CoreBundle', 'entity' => 'MessageTag', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
251
            ['bundle' => 'CoreBundle', 'entity' => 'Notification', 'field' => 'destUserId', 'type' => 'int', 'action' => 'delete'],
252
            ['bundle' => 'CoreBundle', 'entity' => 'PageCategory', 'field' => 'creator', 'type' => 'object', 'action' => 'convert'],
253
            // ['bundle' => 'CoreBundle', 'entity' => 'PersonalAgenda', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
254
            ['bundle' => 'CoreBundle', 'entity' => 'Portfolio', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
255
            ['bundle' => 'CoreBundle', 'entity' => 'PortfolioCategory', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
256
            ['bundle' => 'CoreBundle', 'entity' => 'PortfolioComment', 'field' => 'author', 'type' => 'object', 'action' => 'convert'],
257
            ['bundle' => 'CoreBundle', 'entity' => 'ResourceComment', 'field' => 'author', 'type' => 'object', 'action' => 'convert'],
258
            ['bundle' => 'CoreBundle', 'entity' => 'SequenceValue', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
259
            ['bundle' => 'CoreBundle', 'entity' => 'SessionRelCourseRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
260
            ['bundle' => 'CoreBundle', 'entity' => 'SessionRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
261
            ['bundle' => 'CoreBundle', 'entity' => 'SkillRelItemRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
262
            ['bundle' => 'CoreBundle', 'entity' => 'SkillRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
263
            ['bundle' => 'CoreBundle', 'entity' => 'SkillRelUserComment', 'field' => 'feedbackGiver', 'type' => 'object', 'action' => 'delete'],
264
            ['bundle' => 'CoreBundle', 'entity' => 'SocialPost', 'field' => 'sender', 'type' => 'object', 'action' => 'convert'],
265
            ['bundle' => 'CoreBundle', 'entity' => 'SocialPost', 'field' => 'userReceiver', 'type' => 'object', 'action' => 'convert'],
266
            ['bundle' => 'CoreBundle', 'entity' => 'SocialPostAttachment', 'field' => 'insertUserId', 'type' => 'int', 'action' => 'convert'],
267
            ['bundle' => 'CoreBundle', 'entity' => 'SocialPostFeedback', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
268
            ['bundle' => 'CoreBundle', 'entity' => 'Templates', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
269
            ['bundle' => 'CoreBundle', 'entity' => 'TicketAssignedLog', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
270
            ['bundle' => 'CoreBundle', 'entity' => 'TicketCategory', 'field' => 'insertUserId', 'type' => 'int', 'action' => 'convert'],
271
            ['bundle' => 'CoreBundle', 'entity' => 'TicketCategoryRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
272
            ['bundle' => 'CoreBundle', 'entity' => 'TicketMessage', 'field' => 'insertUserId', 'type' => 'int', 'action' => 'convert'],
273
            ['bundle' => 'CoreBundle', 'entity' => 'TicketMessageAttachment', 'field' => 'lastEditUserId', 'type' => 'int', 'action' => 'convert'],
274
            ['bundle' => 'CoreBundle', 'entity' => 'TicketPriority', 'field' => 'insertUserId', 'type' => 'int', 'action' => 'convert'],
275
            ['bundle' => 'CoreBundle', 'entity' => 'TicketProject', 'field' => 'insertUserId', 'type' => 'int', 'action' => 'convert'],
276
            ['bundle' => 'CoreBundle', 'entity' => 'TicketProject', 'field' => 'lastEditUserId', 'type' => 'int', 'action' => 'convert'],
277
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEAccess', 'field' => 'accessUserId', 'type' => 'int', 'action' => 'delete'],
278
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEAccessComplete', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
279
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEAttempt', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
280
            ['bundle' => 'CoreBundle', 'entity' => 'TrackECourseAccess', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
281
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEDefault', 'field' => 'defaultUserId', 'type' => 'int', 'action' => 'convert'],
282
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEDownloads', 'field' => 'downUserId', 'type' => 'int', 'action' => 'delete'],
283
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEExercise', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
284
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEExerciseConfirmation', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
285
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEHotpotatoes', 'field' => 'exeUserId', 'type' => 'int', 'action' => 'delete'],
286
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEHotspot', 'field' => 'hotspotUserId', 'type' => 'int', 'action' => 'delete'],
287
            ['bundle' => 'CoreBundle', 'entity' => 'TrackELastaccess', 'field' => 'accessUserId', 'type' => 'int', 'action' => 'delete'],
288
            ['bundle' => 'CoreBundle', 'entity' => 'TrackELinks', 'field' => 'linksUserId', 'type' => 'int', 'action' => 'delete'],
289
            ['bundle' => 'CoreBundle', 'entity' => 'TrackELogin', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
290
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEOnline', 'field' => 'loginUserId', 'type' => 'int', 'action' => 'delete'],
291
            ['bundle' => 'CoreBundle', 'entity' => 'TrackEUploads', 'field' => 'uploadUserId', 'type' => 'int', 'action' => 'delete'],
292
            ['bundle' => 'CoreBundle', 'entity' => 'UsergroupRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'convert'],
293
            ['bundle' => 'CoreBundle', 'entity' => 'UserRelTag', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
294
            ['bundle' => 'CoreBundle', 'entity' => 'UserRelUser', 'field' => 'user', 'type' => 'object', 'action' => 'delete'],
295
        ];
296
297
        foreach ($relations as $relation) {
298
            $entityClass = 'Chamilo\\'.$relation['bundle'].'\\Entity\\'.$relation['entity'];
299
            $repository = $em->getRepository($entityClass);
300
            $records = $repository->findBy([$relation['field'] => $userToDelete]);
301
302
            foreach ($records as $record) {
303
                $setter = 'set'.ucfirst($relation['field']);
304
                if ('delete' === $relation['action']) {
305
                    $em->remove($record);
306
                } elseif (method_exists($record, $setter)) {
307
                    $valueToSet = 'object' === $relation['type'] ? $fallbackUser : $fallbackUser->getId();
308
                    $record->{$setter}($valueToSet);
309
                    if (method_exists($record, 'getResourceFile') && $record->getResourceFile()) {
310
                        $resourceFile = $record->getResourceFile();
311
                        if (!$em->contains($resourceFile)) {
312
                            $em->persist($resourceFile);
313
                        }
314
                    }
315
                    $em->persist($record);
316
                }
317
            }
318
        }
319
320
        $em->flush();
321
    }
322
323
    public function getFallbackUser(): ?User
324
    {
325
        return $this->findOneBy(['status' => User::ROLE_FALLBACK], ['id' => 'ASC']);
326
    }
327
328
    public function addUserToResourceNode(int $userId, int $creatorId): ResourceNode
329
    {
330
        /** @var User $user */
331
        $user = $this->find($userId);
332
        $creator = $this->find($creatorId);
333
334
        $resourceNode = (new ResourceNode())
335
            ->setTitle($user->getUsername())
336
            ->setCreator($creator)
337
            ->setResourceType($this->getResourceType())
338
            // ->setParent($resourceNode)
339
        ;
340
341
        $user->setResourceNode($resourceNode);
342
343
        $this->getEntityManager()->persist($resourceNode);
344
        $this->getEntityManager()->persist($user);
345
346
        return $resourceNode;
347
    }
348
349
    public function addRoleListQueryBuilder(array $roles, ?QueryBuilder $qb = null): QueryBuilder
350
    {
351
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
352
        if (!empty($roles)) {
353
            $orX = $qb->expr()->orX();
354
            foreach ($roles as $role) {
355
                $orX->add($qb->expr()->like('u.roles', ':'.$role));
356
                $qb->setParameter($role, '%'.$role.'%');
357
            }
358
            $qb->andWhere($orX);
359
        }
360
361
        return $qb;
362
    }
363
364
    public function findByUsername(string $username): ?User
365
    {
366
        $user = $this->findOneBy([
367
            'username' => $username,
368
        ]);
369
370
        if (null === $user) {
371
            throw new UserNotFoundException(sprintf("User with id '%s' not found.", $username));
372
        }
373
374
        return $user;
375
    }
376
377
    /**
378
     * Get a filtered list of user by role and (optionally) access url.
379
     *
380
     * @param string $keyword     The query to filter
381
     * @param int    $accessUrlId The access URL ID
382
     *
383
     * @return User[]
384
     */
385
    public function findByRole(string $role, string $keyword, int $accessUrlId = 0)
386
    {
387
        $qb = $this->createQueryBuilder('u');
388
389
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
390
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
391
        $this->addRoleQueryBuilder($role, $qb);
392
        $this->addSearchByKeywordQueryBuilder($keyword, $qb);
393
394
        return $qb->getQuery()->getResult();
395
    }
396
397
    public function findByRoleList(array $roleList, string $keyword, int $accessUrlId = 0)
398
    {
399
        $qb = $this->createQueryBuilder('u');
400
401
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
402
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
403
        $this->addRoleListQueryBuilder($roleList, $qb);
404
        $this->addSearchByKeywordQueryBuilder($keyword, $qb);
405
406
        return $qb->getQuery()->getResult();
407
    }
408
409
    /**
410
     * Get the coaches for a course within a session.
411
     *
412
     * @return Collection|array
413
     */
414
    public function getCoachesForSessionCourse(Session $session, Course $course)
415
    {
416
        $qb = $this->createQueryBuilder('u');
417
418
        $qb->select('u')
419
            ->innerJoin(
420
                'ChamiloCoreBundle:SessionRelCourseRelUser',
421
                'scu',
422
                Join::WITH,
423
                'scu.user = u'
424
            )
425
            ->where(
426
                $qb->expr()->andX(
427
                    $qb->expr()->eq('scu.session', $session->getId()),
428
                    $qb->expr()->eq('scu.course', $course->getId()),
429
                    $qb->expr()->eq('scu.status', Session::COURSE_COACH)
430
                )
431
            )
432
        ;
433
434
        return $qb->getQuery()->getResult();
435
    }
436
437
    /**
438
     * Get the sessions admins for a user.
439
     *
440
     * @return array
441
     */
442
    public function getSessionAdmins(User $user)
443
    {
444
        $qb = $this->createQueryBuilder('u');
445
        $qb
446
            ->distinct()
447
            ->innerJoin(
448
                'ChamiloCoreBundle:SessionRelUser',
449
                'su',
450
                Join::WITH,
451
                'u = su.user'
452
            )
453
            ->innerJoin(
454
                'ChamiloCoreBundle:SessionRelCourseRelUser',
455
                'scu',
456
                Join::WITH,
457
                'su.session = scu.session'
458
            )
459
            ->where(
460
                $qb->expr()->eq('scu.user', $user->getId())
461
            )
462
            ->andWhere(
463
                $qb->expr()->eq('su.relationType', Session::DRH)
464
            )
465
        ;
466
467
        return $qb->getQuery()->getResult();
468
    }
469
470
    /**
471
     * Get number of users in URL.
472
     *
473
     * @return int
474
     */
475
    public function getCountUsersByUrl(AccessUrl $url)
476
    {
477
        return $this->createQueryBuilder('u')
478
            ->select('COUNT(u)')
479
            ->innerJoin('u.portals', 'p')
480
            ->where('p.url = :url')
481
            ->setParameters([
482
                'url' => $url,
483
            ])
484
            ->getQuery()
485
            ->getSingleScalarResult()
486
        ;
487
    }
488
489
    /**
490
     * Get number of users in URL.
491
     *
492
     * @return int
493
     */
494
    public function getCountTeachersByUrl(AccessUrl $url)
495
    {
496
        $qb = $this->createQueryBuilder('u');
497
498
        $qb
499
            ->select('COUNT(u)')
500
            ->innerJoin('u.portals', 'p')
501
            ->where('p.url = :url')
502
            ->setParameters([
503
                'url' => $url,
504
            ])
505
        ;
506
507
        $this->addRoleListQueryBuilder(['ROLE_TEACHER'], $qb);
508
509
        return (int) $qb->getQuery()->getSingleScalarResult();
510
    }
511
512
    /**
513
     * Find potential users to send a message.
514
     *
515
     * @todo remove  api_is_platform_admin
516
     *
517
     * @param int    $currentUserId The current user ID
518
     * @param string $searchFilter  Optional. The search text to filter the user list
519
     * @param int    $limit         Optional. Sets the maximum number of results to retrieve
520
     *
521
     * @return User[]
522
     */
523
    public function findUsersToSendMessage(int $currentUserId, ?string $searchFilter = null, int $limit = 10)
524
    {
525
        $allowSendMessageToAllUsers = api_get_setting('allow_send_message_to_all_platform_users');
526
        $accessUrlId = api_get_multiple_access_url() ? api_get_current_access_url_id() : 1;
527
528
        $messageTool = 'true' === api_get_setting('allow_message_tool');
529
        if (!$messageTool) {
530
            return [];
531
        }
532
533
        $qb = $this->createQueryBuilder('u');
534
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
535
        $this->addAccessUrlQueryBuilder($accessUrlId, $qb);
536
537
        $dql = null;
538
        if ('true' === api_get_setting('allow_social_tool')) {
539
            // All users
540
            if ('true' === $allowSendMessageToAllUsers || api_is_platform_admin()) {
541
                $this->addNotCurrentUserQueryBuilder($currentUserId, $qb);
542
            /*$dql = "SELECT DISTINCT U
543
                    FROM ChamiloCoreBundle:User U
544
                    LEFT JOIN ChamiloCoreBundle:AccessUrlRelUser R
545
                    WITH U = R.user
546
                    WHERE
547
                        U.active = 1 AND
548
                        U.status != 6  AND
549
                        U.id != {$currentUserId} AND
550
                        R.url = {$accessUrlId}";*/
551
            } else {
552
                $this->addOnlyMyFriendsQueryBuilder($currentUserId, $qb);
553
                /*$dql = 'SELECT DISTINCT U
554
                        FROM ChamiloCoreBundle:AccessUrlRelUser R, ChamiloCoreBundle:UserRelUser UF
555
                        INNER JOIN ChamiloCoreBundle:User AS U
556
                        WITH UF.friendUserId = U
557
                        WHERE
558
                            U.active = 1 AND
559
                            U.status != 6 AND
560
                            UF.relationType NOT IN('.USER_RELATION_TYPE_DELETED.', '.USER_RELATION_TYPE_RRHH.") AND
561
                            UF.user = {$currentUserId} AND
562
                            UF.friendUserId != {$currentUserId} AND
563
                            U = R.user AND
564
                            R.url = {$accessUrlId}";*/
565
            }
566
        } else {
567
            if ('true' === $allowSendMessageToAllUsers) {
568
                $this->addNotCurrentUserQueryBuilder($currentUserId, $qb);
569
            } else {
570
                return [];
571
            }
572
573
            /*else {
574
                $time_limit = (int) api_get_setting('time_limit_whosonline');
575
                $online_time = time() - ($time_limit * 60);
576
                $limit_date = api_get_utc_datetime($online_time);
577
                $dql = "SELECT DISTINCT U
578
                        FROM ChamiloCoreBundle:User U
579
                        INNER JOIN ChamiloCoreBundle:TrackEOnline T
580
                        WITH U.id = T.loginUserId
581
                        WHERE
582
                          U.active = 1 AND
583
                          T.loginDate >= '".$limit_date."'";
584
            }*/
585
        }
586
587
        if (!empty($searchFilter)) {
588
            $this->addSearchByKeywordQueryBuilder($searchFilter, $qb);
589
        }
590
591
        return $qb->getQuery()->getResult();
592
    }
593
594
    /**
595
     * Get the list of HRM who have assigned this user.
596
     *
597
     * @return User[]
598
     */
599
    public function getAssignedHrmUserList(int $userId, int $urlId)
600
    {
601
        $qb = $this->createQueryBuilder('u');
602
        $this->addAccessUrlQueryBuilder($urlId, $qb);
603
        $this->addActiveAndNotAnonUserQueryBuilder($qb);
604
        $this->addUserRelUserQueryBuilder($userId, UserRelUser::USER_RELATION_TYPE_RRHH, $qb);
605
606
        return $qb->getQuery()->getResult();
607
    }
608
609
    /**
610
     * Get the last login from the track_e_login table.
611
     * This might be different from user.last_login in the case of legacy users
612
     * as user.last_login was only implemented in 1.10 version with a default
613
     * value of NULL (not the last record from track_e_login).
614
     *
615
     * @return null|TrackELogin
616
     */
617
    public function getLastLogin(User $user)
618
    {
619
        $qb = $this->createQueryBuilder('u');
620
621
        return $qb
622
            ->select('l')
623
            ->innerJoin('u.logins', 'l')
624
            ->where(
625
                $qb->expr()->eq('l.user', $user)
626
            )
627
            ->setMaxResults(1)
628
            ->orderBy('u.loginDate', Criteria::DESC)
629
            ->getQuery()
630
            ->getOneOrNullResult()
631
        ;
632
    }
633
634
    public function addAccessUrlQueryBuilder(int $accessUrlId, ?QueryBuilder $qb = null): QueryBuilder
635
    {
636
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
637
        $qb
638
            ->innerJoin('u.portals', 'p')
639
            ->andWhere('p.url = :url')
640
            ->setParameter('url', $accessUrlId, Types::INTEGER)
641
        ;
642
643
        return $qb;
644
    }
645
646
    public function addActiveAndNotAnonUserQueryBuilder(?QueryBuilder $qb = null): QueryBuilder
647
    {
648
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
649
        $qb
650
            ->andWhere('u.active = 1')
651
            ->andWhere('u.status <> :status')
652
            ->setParameter('status', User::ANONYMOUS, Types::INTEGER)
653
        ;
654
655
        return $qb;
656
    }
657
658
    public function addExpirationDateQueryBuilder(?QueryBuilder $qb = null): QueryBuilder
659
    {
660
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
661
        $qb
662
            ->andWhere('u.expirationDate IS NULL OR u.expirationDate > :now')
663
            ->setParameter('now', new Datetime(), Types::DATETIME_MUTABLE)
664
        ;
665
666
        return $qb;
667
    }
668
669
    private function addRoleQueryBuilder(string $role, ?QueryBuilder $qb = null): QueryBuilder
670
    {
671
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
672
        $qb
673
            ->andWhere('u.roles LIKE :roles')
674
            ->setParameter('roles', '%"'.$role.'"%', Types::STRING)
675
        ;
676
677
        return $qb;
678
    }
679
680
    private function addSearchByKeywordQueryBuilder(string $keyword, ?QueryBuilder $qb = null): QueryBuilder
681
    {
682
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
683
        $qb
684
            ->andWhere('
685
                u.firstname LIKE :keyword OR
686
                u.lastname LIKE :keyword OR
687
                u.email LIKE :keyword OR
688
                u.username LIKE :keyword
689
            ')
690
            ->setParameter('keyword', "%$keyword%", Types::STRING)
691
            ->orderBy('u.firstname', Criteria::ASC)
692
        ;
693
694
        return $qb;
695
    }
696
697
    private function addUserRelUserQueryBuilder(int $userId, int $relationType, ?QueryBuilder $qb = null): QueryBuilder
698
    {
699
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
700
        $qb->leftJoin('u.friends', 'relations');
701
        $qb
702
            ->andWhere('relations.relationType = :relationType')
703
            ->andWhere('relations.user = :userRelation AND relations.friend <> :userRelation')
704
            ->setParameter('relationType', $relationType)
705
            ->setParameter('userRelation', $userId)
706
        ;
707
708
        return $qb;
709
    }
710
711
    private function addOnlyMyFriendsQueryBuilder(int $userId, ?QueryBuilder $qb = null): QueryBuilder
712
    {
713
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
714
        $qb
715
            ->leftJoin('u.friends', 'relations')
716
            ->andWhere(
717
                $qb->expr()->notIn(
718
                    'relations.relationType',
719
                    [UserRelUser::USER_RELATION_TYPE_DELETED, UserRelUser::USER_RELATION_TYPE_RRHH]
720
                )
721
            )
722
            ->andWhere('relations.user = :user AND relations.friend <> :user')
723
            ->setParameter('user', $userId, Types::INTEGER)
724
        ;
725
726
        return $qb;
727
    }
728
729
    private function addNotCurrentUserQueryBuilder(int $userId, ?QueryBuilder $qb = null): QueryBuilder
730
    {
731
        $qb = $this->getOrCreateQueryBuilder($qb, 'u');
732
        $qb
733
            ->andWhere('u.id <> :id')
734
            ->setParameter('id', $userId, Types::INTEGER)
735
        ;
736
737
        return $qb;
738
    }
739
740
    public function getFriendsNotInGroup(int $userId, int $groupId)
741
    {
742
        $entityManager = $this->getEntityManager();
743
744
        $subQueryBuilder = $entityManager->createQueryBuilder();
745
        $subQuery = $subQueryBuilder
746
            ->select('IDENTITY(ugr.user)')
747
            ->from(UsergroupRelUser::class, 'ugr')
748
            ->where('ugr.usergroup = :subGroupId')
749
            ->andWhere('ugr.relationType IN (:subRelationTypes)')
750
            ->getDQL()
751
        ;
752
753
        $queryBuilder = $entityManager->createQueryBuilder();
754
        $query = $queryBuilder
755
            ->select('u')
756
            ->from(User::class, 'u')
757
            ->leftJoin('u.friendsWithMe', 'uruf')
758
            ->leftJoin('u.friends', 'urut')
759
            ->where('uruf.friend = :userId OR urut.user = :userId')
760
            ->andWhere($queryBuilder->expr()->notIn('u.id', $subQuery))
761
            ->setParameter('userId', $userId)
762
            ->setParameter('subGroupId', $groupId)
763
            ->setParameter('subRelationTypes', [Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION])
764
            ->getQuery()
765
        ;
766
767
        return $query->getResult();
768
    }
769
770
    public function getExtraUserData(int $userId, bool $prefix = false, bool $allVisibility = true, bool $splitMultiple = false, ?int $fieldFilter = null): array
771
    {
772
        $qb = $this->getEntityManager()->createQueryBuilder();
773
774
        // Start building the query
775
        $qb->select('ef.id', 'ef.variable as fvar', 'ef.valueType as type', 'efv.fieldValue as fval', 'ef.defaultValue as fval_df')
776
            ->from(ExtraField::class, 'ef')
777
            ->leftJoin(ExtraFieldValues::class, 'efv', Join::WITH, 'efv.field = ef.id AND efv.itemId = :userId')
778
            ->where('ef.itemType = :itemType')
779
            ->setParameter('userId', $userId)
780
            ->setParameter('itemType', ExtraField::USER_FIELD_TYPE)
781
        ;
782
783
        // Apply visibility filters
784
        if (!$allVisibility) {
785
            $qb->andWhere('ef.visibleToSelf = true');
786
        }
787
788
        // Apply field filter if provided
789
        if (null !== $fieldFilter) {
790
            $qb->andWhere('ef.id = :fieldFilter')
791
                ->setParameter('fieldFilter', $fieldFilter)
792
            ;
793
        }
794
795
        // Order by field order
796
        $qb->orderBy('ef.fieldOrder', 'ASC');
797
798
        // Execute the query
799
        $results = $qb->getQuery()->getResult();
800
801
        // Process results
802
        $extraData = [];
803
        foreach ($results as $row) {
804
            $value = $row['fval'] ?? $row['fval_df'];
805
806
            // Handle multiple values if necessary
807
            if ($splitMultiple && \in_array($row['type'], [ExtraField::USER_FIELD_TYPE_SELECT_MULTIPLE], true)) {
808
                $value = explode(';', $value);
809
            }
810
811
            // Handle prefix if needed
812
            $key = $prefix ? 'extra_'.$row['fvar'] : $row['fvar'];
813
814
            // Special handling for certain field types
815
            if (ExtraField::USER_FIELD_TYPE_TAG == $row['type']) {
816
                // Implement your logic to handle tags
817
            } elseif (ExtraField::USER_FIELD_TYPE_RADIO == $row['type'] && $prefix) {
818
                $extraData[$key][$key] = $value;
819
            } else {
820
                $extraData[$key] = $value;
821
            }
822
        }
823
824
        return $extraData;
825
    }
826
827
    public function getExtraUserDataByField(int $userId, string $fieldVariable, bool $allVisibility = true): array
828
    {
829
        $qb = $this->getEntityManager()->createQueryBuilder();
830
831
        $qb->select('e.id, e.variable, e.valueType, v.fieldValue')
832
            ->from(ExtraFieldValues::class, 'v')
833
            ->innerJoin('v.field', 'e')
834
            ->where('v.itemId = :userId')
835
            ->andWhere('e.variable = :fieldVariable')
836
            ->andWhere('e.itemType = :itemType')
837
            ->setParameters([
838
                'userId' => $userId,
839
                'fieldVariable' => $fieldVariable,
840
                'itemType' => ExtraField::USER_FIELD_TYPE,
841
            ])
842
        ;
843
844
        if (!$allVisibility) {
845
            $qb->andWhere('e.visibleToSelf = true');
846
        }
847
848
        $qb->orderBy('e.fieldOrder', 'ASC');
849
850
        $result = $qb->getQuery()->getResult();
851
852
        $extraData = [];
853
        foreach ($result as $row) {
854
            $value = $row['fieldValue'];
855
            if (ExtraField::USER_FIELD_TYPE_SELECT_MULTIPLE == $row['valueType']) {
856
                $value = explode(';', $row['fieldValue']);
857
            }
858
859
            $extraData[$row['variable']] = $value;
860
        }
861
862
        return $extraData;
863
    }
864
865
    public function searchUsersByTags(
866
        string $tag,
867
        ?int $excludeUserId = null,
868
        int $fieldId = 0,
869
        int $from = 0,
870
        int $number_of_items = 10,
871
        bool $getCount = false
872
    ): array {
873
        $qb = $this->createQueryBuilder('u');
874
875
        if ($getCount) {
876
            $qb->select('COUNT(DISTINCT u.id)');
877
        } else {
878
            $qb->select('DISTINCT u.id, u.username, u.firstname, u.lastname, u.email, u.pictureUri, u.status');
879
        }
880
881
        $qb->innerJoin('u.portals', 'urlRelUser')
882
            ->leftJoin(UserRelTag::class, 'uv', 'WITH', 'u = uv.user')
883
            ->leftJoin(Tag::class, 'ut', 'WITH', 'uv.tag = ut')
884
        ;
885
886
        if (0 !== $fieldId) {
887
            $qb->andWhere('ut.field = :fieldId')
888
                ->setParameter('fieldId', $fieldId)
889
            ;
890
        }
891
892
        if (null !== $excludeUserId) {
893
            $qb->andWhere('u.id != :excludeUserId')
894
                ->setParameter('excludeUserId', $excludeUserId)
895
            ;
896
        }
897
898
        $qb->andWhere(
899
            $qb->expr()->orX(
900
                $qb->expr()->like('ut.tag', ':tag'),
901
                $qb->expr()->like('u.firstname', ':likeTag'),
902
                $qb->expr()->like('u.lastname', ':likeTag'),
903
                $qb->expr()->like('u.username', ':likeTag'),
904
                $qb->expr()->like(
905
                    $qb->expr()->concat('u.firstname', $qb->expr()->literal(' '), 'u.lastname'),
906
                    ':likeTag'
907
                ),
908
                $qb->expr()->like(
909
                    $qb->expr()->concat('u.lastname', $qb->expr()->literal(' '), 'u.firstname'),
910
                    ':likeTag'
911
                )
912
            )
913
        )
914
            ->setParameter('tag', $tag.'%')
915
            ->setParameter('likeTag', '%'.$tag.'%')
916
        ;
917
918
        // Only active users and not anonymous
919
        $qb->andWhere('u.active = :active')
920
            ->andWhere('u.status != :anonymous')
921
            ->setParameter('active', true)
922
            ->setParameter('anonymous', 6)
923
        ;
924
925
        if (!$getCount) {
926
            $qb->orderBy('u.username')
927
                ->setFirstResult($from)
928
                ->setMaxResults($number_of_items)
929
            ;
930
        }
931
932
        return $getCount ? $qb->getQuery()->getSingleScalarResult() : $qb->getQuery()->getResult();
933
    }
934
935
    public function getUserRelationWithType(int $userId, int $friendId): ?array
936
    {
937
        $qb = $this->createQueryBuilder('u');
938
        $qb->select('u.id AS userId', 'u.username AS userName', 'ur.relationType', 'f.id AS friendId', 'f.username AS friendName')
939
            ->innerJoin('u.friends', 'ur')
940
            ->innerJoin('ur.friend', 'f')
941
            ->where('u.id = :userId AND f.id = :friendId')
942
            ->setParameter('userId', $userId)
943
            ->setParameter('friendId', $friendId)
944
            ->setMaxResults(1)
945
        ;
946
947
        return $qb->getQuery()->getOneOrNullResult();
948
    }
949
950
    public function relateUsers(User $user1, User $user2, int $relationType): void
951
    {
952
        $em = $this->getEntityManager();
953
954
        $existingRelation = $em->getRepository(UserRelUser::class)->findOneBy([
955
            'user' => $user1,
956
            'friend' => $user2,
957
        ]);
958
959
        if (!$existingRelation) {
960
            $newRelation = new UserRelUser();
961
            $newRelation->setUser($user1);
962
            $newRelation->setFriend($user2);
963
            $newRelation->setRelationType($relationType);
964
            $em->persist($newRelation);
965
        } else {
966
            $existingRelation->setRelationType($relationType);
967
        }
968
969
        $existingRelationInverse = $em->getRepository(UserRelUser::class)->findOneBy([
970
            'user' => $user2,
971
            'friend' => $user1,
972
        ]);
973
974
        if (!$existingRelationInverse) {
975
            $newRelationInverse = new UserRelUser();
976
            $newRelationInverse->setUser($user2);
977
            $newRelationInverse->setFriend($user1);
978
            $newRelationInverse->setRelationType($relationType);
979
            $em->persist($newRelationInverse);
980
        } else {
981
            $existingRelationInverse->setRelationType($relationType);
982
        }
983
984
        $em->flush();
985
    }
986
987
    public function getUserPicture(
988
        $userId,
989
        int $size = self::USER_IMAGE_SIZE_MEDIUM,
990
        $addRandomId = true,
991
    ) {
992
        $user = $this->find($userId);
993
        if (!$user) {
994
            return '/img/icons/64/unknown.png';
995
        }
996
997
        switch ($size) {
998
            case self::USER_IMAGE_SIZE_SMALL:
999
                $width = 32;
1000
1001
                break;
1002
1003
            case self::USER_IMAGE_SIZE_MEDIUM:
1004
                $width = 64;
1005
1006
                break;
1007
1008
            case self::USER_IMAGE_SIZE_BIG:
1009
                $width = 128;
1010
1011
                break;
1012
1013
            case self::USER_IMAGE_SIZE_ORIGINAL:
1014
            default:
1015
                $width = 0;
1016
1017
                break;
1018
        }
1019
1020
        $url = $this->illustrationRepository->getIllustrationUrl($user);
1021
        $params = [];
1022
        if (!empty($width)) {
1023
            $params['w'] = $width;
1024
        }
1025
1026
        if ($addRandomId) {
1027
            $params['rand'] = uniqid('u_', true);
1028
        }
1029
1030
        $paramsToString = '';
1031
        if (!empty($params)) {
1032
            $paramsToString = '?'.http_build_query($params);
1033
        }
1034
1035
        return $url.$paramsToString;
1036
    }
1037
}
1038