Passed
Pull Request — master (#6922)
by
unknown
08:21
created

upsertCertificateResource()   B

Complexity

Conditions 11
Paths 49

Size

Total Lines 71
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 36
nc 49
nop 5
dl 0
loc 71
rs 7.3166
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Repository;
8
9
use Chamilo\CoreBundle\Entity\GradebookCategory;
10
use Chamilo\CoreBundle\Entity\GradebookCertificate;
11
use Chamilo\CoreBundle\Entity\PersonalFile;
12
use Chamilo\CoreBundle\Entity\ResourceNode;
13
use Chamilo\CoreBundle\Entity\ResourceType;
14
use Chamilo\CoreBundle\Entity\SessionRelCourseRelUser;
15
use Chamilo\CoreBundle\Entity\SessionRelUser;
16
use Chamilo\CoreBundle\Entity\User;
17
use DateTime;
18
use DateTimeImmutable;
19
use Doctrine\ORM\AbstractQuery;
20
use Doctrine\ORM\NonUniqueResultException;
21
use Doctrine\Persistence\ManagerRegistry;
22
use Symfony\Component\HttpFoundation\File\UploadedFile;
23
24
class GradebookCertificateRepository extends ResourceRepository
25
{
26
    public function __construct(ManagerRegistry $registry)
27
    {
28
        parent::__construct($registry, GradebookCertificate::class);
29
    }
30
31
    /**
32
     * Fetch a certificate by (user, category). If $catId is 0 or null, searches the "general" certificate.
33
     */
34
    public function getCertificateByUserId(?int $catId, int $userId, bool $asArray = false)
35
    {
36
        $qb = $this->createQueryBuilder('gc')
37
            ->where('gc.user = :userId')
38
            ->setParameter('userId', $userId)
39
            ->setMaxResults(1);
40
41
        if (0 === $catId) {
42
            $catId = null;
43
        }
44
45
        if (null === $catId) {
46
            $qb->andWhere('gc.category IS NULL');
47
        } else {
48
            $qb->andWhere('gc.category = :catId')
49
                ->setParameter('catId', $catId);
50
        }
51
52
        $qb->orderBy('gc.id', 'ASC');
53
        $query = $qb->getQuery();
54
55
        try {
56
            return $asArray
57
                ? $query->getOneOrNullResult(AbstractQuery::HYDRATE_ARRAY)
58
                : $query->getOneOrNullResult();
59
        } catch (NonUniqueResultException $e) {
60
            return null;
61
        }
62
    }
63
64
    /**
65
     * Backward-compatible metadata update.
66
     * If you adopt the Resource flow, you may pass an empty $fileName; it will still update score/timestamps.
67
     */
68
    public function registerUserInfoAboutCertificate(int $catId, int $userId, float $scoreCertificate, string $fileName = ''): void
69
    {
70
        $fileName = ltrim($fileName, '/');
71
        $existingCertificate = $this->getCertificateByUserId(0 === $catId ? null : $catId, $userId);
72
73
        if (!$existingCertificate) {
74
            $certificate = new GradebookCertificate();
75
76
            $category = 0 === $catId ? null : $this->_em->getRepository(GradebookCategory::class)->find($catId);
77
            $user = $this->_em->getRepository(User::class)->find($userId);
78
79
            if ($category) {
80
                $certificate->setCategory($category);
81
            }
82
            $certificate->setUser($user);
83
            $certificate->setPathCertificate($fileName ?: null);
84
            $certificate->setScoreCertificate($scoreCertificate);
85
            $certificate->setCreatedAt(new DateTime());
86
87
            $this->_em->persist($certificate);
88
            $this->_em->flush();
89
90
            return;
91
        }
92
93
        if ($fileName) {
94
            $existingCertificate->setPathCertificate($fileName);
95
        }
96
        $existingCertificate->setScoreCertificate($scoreCertificate);
97
        $this->_em->flush();
98
    }
99
100
    /**
101
     * Helper: resolve the ResourceType 'files' from the Tool 'files' (personal space).
102
     *
103
     * @throws \RuntimeException if not found.
104
     */
105
    private function getPersonalFilesResourceType(): ResourceType
106
    {
107
        $em = $this->getEntityManager();
108
109
        $qb = $em->createQueryBuilder()
110
            ->select('rt')
111
            ->from(ResourceType::class, 'rt')
112
            ->join('rt.tool', 't')
113
            ->where('rt.title = :rtTitle')
114
            ->andWhere('t.title = :toolTitle')
115
            ->setParameter('rtTitle', 'files')
116
            ->setParameter('toolTitle', 'files')
117
            ->setMaxResults(1);
118
119
        $rt = $qb->getQuery()->getOneOrNullResult();
120
        if ($rt instanceof ResourceType) {
121
            return $rt;
122
        }
123
124
        $rt = $em->getRepository(ResourceType::class)->findOneBy(['title' => 'files']);
125
        if (!$rt) {
126
            throw new \RuntimeException("ResourceType 'files' not found.");
127
        }
128
129
        return $rt;
130
    }
131
132
    /**
133
     * Create or update a certificate as a Resource using ResourceType 'files' (personal files tool).
134
     * We avoid calling parent::createNodeForResource() to not depend on $this->slugify.
135
     */
136
    public function upsertCertificateResource(
137
        int $catId,
138
        int $userId,
139
        float $scoreCertificate,
140
        string $htmlContent,
141
        ?string $pdfBinary = null
142
    ): GradebookCertificate {
143
        $em = $this->getEntityManager();
144
145
        /** @var GradebookCertificate|null $cert */
146
        $cert = $this->getCertificateByUserId(0 === $catId ? null : $catId, $userId);
147
148
        /** @var User|null $user */
149
        $user = $em->getRepository(User::class)->find($userId);
150
        if (!$user) {
151
            throw new \InvalidArgumentException("User {$userId} not found.");
152
        }
153
154
        /** @var GradebookCategory|null $category */
155
        $category = 0 === $catId ? null : $em->getRepository(GradebookCategory::class)->find($catId);
156
157
        if (!$cert) {
158
            $cert = new GradebookCertificate();
159
            if ($category) {
160
                $cert->setCategory($category);
161
            }
162
            $cert->setUser($user);
163
            $cert->setScoreCertificate($scoreCertificate);
164
            $cert->setCreatedAt(new DateTime());
165
            $em->persist($cert);
166
            $em->flush();
167
        } else {
168
            $cert->setScoreCertificate($scoreCertificate);
169
        }
170
171
        // Deterministic filename for legacy parity (used historically to build URLs)
172
        $logicalFileName = ltrim(hash('sha256', $userId . ($catId ?: 0)) . '.html', '/');
173
174
        // Ensure resource node exists with ResourceType 'files', under the user's node
175
        if (!$cert->hasResourceNode()) {
176
            $filesRt    = $this->getPersonalFilesResourceType();
177
            $parentNode = $user->getResourceNode();
178
            $this->createNodeForCertificateWithoutSlugify($cert, $user, $parentNode, $filesRt, $logicalFileName);
179
180
            // Link to the user (so it appears in their resources)
181
            $cert->addUserLink($user);
182
        } else {
183
            // Keep the resource title coherent (not mandatory but nice to have)
184
            $node = $cert->getResourceNode();
185
            $node->setTitle($logicalFileName);
186
            $em->persist($node);
187
        }
188
189
        // Update or create the HTML ResourceFile
190
        $updated = $this->updateResourceFileContent($cert, $htmlContent);
191
        if (!$updated) {
192
            $this->addFileFromString($cert, $logicalFileName, 'text/html', $htmlContent, true);
193
        }
194
195
        // Optional PDF attachment
196
        if (null !== $pdfBinary) {
197
            $pdfName = preg_replace('/\.html$/i', '.pdf', $logicalFileName) ?: ($logicalFileName . '.pdf');
198
            $this->addFileFromString($cert, $pdfName, 'application/pdf', $pdfBinary, true);
199
        }
200
201
        // Legacy pointer (not strictly required for the Resource flow)
202
        $cert->setPathCertificate($logicalFileName);
203
204
        $em->flush();
205
206
        return $cert;
207
    }
208
209
    /**
210
     * Create a ResourceNode avoiding the slugify service.
211
     * Generates a basic slug locally and wires parent/creator/type relationships.
212
     */
213
    private function createNodeForCertificateWithoutSlugify(
214
        GradebookCertificate $resource,
215
        User $creator,
216
        ResourceNode $parentNode,
217
        ResourceType $resourceType,
218
        string $titleForNode
219
    ): void {
220
        $em = $this->getEntityManager();
221
222
        $slug = $this->basicSlug($titleForNode);
223
224
        $node = new ResourceNode();
225
        $node
226
            ->setTitle($titleForNode)
227
            ->setSlug($slug)
228
            ->setResourceType($resourceType)
229
            ->setCreator($creator)
230
        ;
231
232
        // Parent-child relationship
233
        $parentNode?->addChild($node);
234
235
        // Wire resource <-> node
236
        $resource->setResourceNode($node);
237
238
        // Persist
239
        $em->persist($node);
240
        $em->persist($resource);
241
    }
242
243
    /**
244
     * Very small local slugger (ASCII-only fallback).
245
     */
246
    private function basicSlug(string $text): string
247
    {
248
        $text = \iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
249
        $text = \preg_replace('~[^\\pL\\d]+~u', '-', $text ?? '');
250
        $text = \trim((string) $text, '-');
251
        $text = \preg_replace('~[^-a-z0-9]+~i', '', $text);
252
        $text = \strtolower($text);
253
        return $text ?: 'resource';
254
    }
255
256
    /**
257
     * LEGACY (kept for backward-compatibility):
258
     * Creates a PersonalFile under the user's personal files (old behavior).
259
     *
260
     * @deprecated Prefer upsertCertificateResource().
261
     */
262
    public function generateCertificatePersonalFile(int $userId, string $fileName, string $certificateContent): ?PersonalFile
263
    {
264
        $em = $this->getEntityManager();
265
        $userEntity = $em->getRepository(User::class)->find($userId);
266
267
        $existingFile = $em->getRepository(PersonalFile::class)->findOneBy(['title' => $fileName]);
268
269
        if (!$existingFile) {
270
            $tempFilePath = tempnam(sys_get_temp_dir(), 'cert');
271
            file_put_contents($tempFilePath, $certificateContent);
272
273
            $mimeType = mime_content_type($tempFilePath);
274
            $uploadedFile = new UploadedFile($tempFilePath, $fileName, $mimeType, null, true);
275
276
            $personalFile = new PersonalFile();
277
            $personalFile->setTitle($fileName);
278
            $personalFile->setCreator($userEntity);
279
            $personalFile->setParentResourceNode($userEntity->getResourceNode()->getId());
280
            $personalFile->setResourceName($fileName);
281
            $personalFile->setUploadFile($uploadedFile);
282
            $personalFile->addUserLink($userEntity);
283
284
            $em->persist($personalFile);
285
            $em->flush();
286
287
            @unlink($tempFilePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

287
            /** @scrutinizer ignore-unhandled */ @unlink($tempFilePath);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
288
289
            return $personalFile;
290
        }
291
292
        return $existingFile;
293
    }
294
295
    /**
296
     * Delete the certificate and its associated files.
297
     * - New mode (resource): hardDelete() removes node + files + links.
298
     * - Legacy mode (personal file): remove PersonalFile and then the row.
299
     */
300
    public function deleteCertificateAndRelatedFiles(int $userId, int $catId): bool
301
    {
302
        $em = $this->getEntityManager();
303
        /** @var GradebookCertificate|null $certificate */
304
        $certificate = $this->getCertificateByUserId($catId, $userId);
305
306
        if (!$certificate) {
307
            return false;
308
        }
309
310
        // attached ResourceNode -> hard delete (cascades)
311
        if ($certificate->hasResourceNode()) {
312
            $this->hardDelete($certificate);
313
            return true;
314
        }
315
316
        // delete PersonalFile created under user's personal files
317
        $title = basename(ltrim((string) $certificate->getPathCertificate(), '/'));
318
        $personalFile = $em->getRepository(PersonalFile::class)->findOneBy(['title' => $title]);
319
320
        if ($personalFile) {
321
            $em->remove($personalFile);
322
            $em->flush();
323
        }
324
325
        $em->remove($certificate);
326
        $em->flush();
327
328
        return true;
329
    }
330
331
    /**
332
     * List certificates with course/session/url context.
333
     */
334
    public function findCertificatesWithContext(
335
        int $urlId,
336
        int $offset = 0,
337
        int $limit = 50
338
    ): array {
339
        return $this->createQueryBuilder('gc')
340
            ->join('gc.category', 'cat')
341
            ->join('cat.course', 'course')
342
            ->join('course.urls', 'curl')
343
            ->join('curl.url', 'url')
344
            ->leftJoin('course.sessions', 'src')
345
            ->leftJoin('src.session', 'session')
346
            ->where('url.id = :urlId')
347
            ->setParameter('urlId', $urlId)
348
            ->orderBy('gc.createdAt', 'DESC')
349
            ->setFirstResult($offset)
350
            ->setMaxResults($limit)
351
            ->getQuery()
352
            ->getResult()
353
        ;
354
    }
355
356
    public function findIncompleteCertificates(int $urlId): array
357
    {
358
        $today = new DateTimeImmutable();
359
        $em = $this->getEntityManager();
360
        $qb = $em->createQueryBuilder();
361
362
        $qb->select('sru', 'u', 's', 'src', 'c')
363
            ->from(SessionRelUser::class, 'sru')
364
            ->join('sru.user', 'u')
365
            ->join('sru.session', 's')
366
            ->join('s.courses', 'src')
367
            ->join('src.course', 'c')
368
            ->join('c.urls', 'curl')
369
            ->join('curl.url', 'url')
370
            ->where('url.id = :urlId')
371
            ->andWhere('s.accessStartDate <= :today')
372
            ->andWhere('s.accessEndDate   IS NULL OR s.accessEndDate > :today')
373
            ->andWhere(
374
                $qb->expr()->not(
375
                    $qb->expr()->exists(
376
                        $em->createQueryBuilder()
377
                            ->select('gc2.id')
378
                            ->from(GradebookCertificate::class, 'gc2')
379
                            ->join('gc2.category', 'cat2')
380
                            ->join('cat2.course', 'cc')
381
                            ->where('gc2.user = u')
382
                            ->andWhere('cc = c')
383
                            ->getDQL()
384
                    )
385
                )
386
            )
387
            ->setParameter('urlId', $urlId)
388
            ->setParameter('today', $today)
389
            ->orderBy('s.accessStartDate', 'DESC')
390
        ;
391
392
        return $qb->getQuery()->getResult();
393
    }
394
395
    public function findRestartableSessions(
396
        int $urlId,
397
        int $offset = 0,
398
        int $limit = 10
399
    ): array {
400
        $today = new DateTimeImmutable();
401
402
        $qb = $this->_em->createQueryBuilder();
403
404
        $qb->select('srcu')
405
            ->from(SessionRelCourseRelUser::class, 'srcu')
406
            ->join('srcu.session', 's')
407
            ->join('srcu.course', 'c')
408
            ->join('c.urls', 'curl')
409
            ->join('curl.url', 'url')
410
            ->where('url.id = :urlId')
411
            ->andWhere('s.accessEndDate IS NOT NULL')
412
            ->andWhere('s.accessEndDate < :today')
413
            ->andWhere(
414
                $qb->expr()->not(
415
                    $qb->expr()->exists(
416
                        $this->_em->createQueryBuilder()
417
                            ->select('gc2.id')
418
                            ->from(GradebookCertificate::class, 'gc2')
419
                            ->join('gc2.category', 'cat2')
420
                            ->join('cat2.course', 'cc')
421
                            ->where('gc2.user = srcu.user')
422
                            ->andWhere('cc = c')
423
                            ->getDQL()
424
                    )
425
                )
426
            )
427
            ->orderBy('s.accessEndDate', 'DESC')
428
            ->setFirstResult($offset)
429
            ->setMaxResults($limit)
430
            ->setParameter('urlId', $urlId)
431
            ->setParameter('today', $today)
432
        ;
433
434
        return $qb->getQuery()->getResult();
435
    }
436
}
437