Passed
Pull Request — master (#6922)
by
unknown
09:08 queued 22s
created

Certificate::generatePdfFromCustomCertificate()   B

Complexity

Conditions 9
Paths 56

Size

Total Lines 42
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 32
c 1
b 0
f 0
nc 56
nop 0
dl 0
loc 42
rs 8.0555
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\GradebookCategory;
6
use Chamilo\CoreBundle\Entity\PersonalFile;
7
use Chamilo\CoreBundle\Entity\ResourceFile;
8
use Chamilo\CoreBundle\Entity\ResourceNode;
9
use Chamilo\CoreBundle\Framework\Container;
10
use Endroid\QrCode\ErrorCorrectionLevel;
11
use Endroid\QrCode\QrCode;
12
use JetBrains\PhpStorm\NoReturn;
13
use Symfony\Component\HttpFoundation\File\UploadedFile;
14
15
/**
16
 * Certificate Class
17
 * Generate certificates based in the gradebook tool.
18
 */
19
class Certificate extends Model
20
{
21
    public $table;
22
    public $columns = [
23
        'id',
24
        'cat_id',
25
        'score_certificate',
26
        'created_at',
27
        'path_certificate',
28
    ];
29
    /**
30
     * Certification data.
31
     */
32
    public $certificate_data = [];
33
34
    /**
35
     * Student's certification path.
36
     */
37
    public $certification_user_path = null;
38
    public $certification_web_user_path = null;
39
    public $html_file = null;
40
    public $qr_file = null;
41
    public $user_id;
42
43
    /** If true every time we enter to the certificate URL
44
     * we would generate a new certificate (good thing because we can edit the
45
     * certificate and all users will have the latest certificate bad because we.
46
     * load the certificate every time */
47
    public $force_certificate_generation = true;
48
49
    /**
50
     * Constructor.
51
     *
52
     * @param int  $certificate_id        ID of the certificate
53
     * @param int  $userId
54
     * @param bool $sendNotification      send message to student
55
     * @param bool $updateCertificateData
56
     * @param string $pathToCertificate
57
     *
58
     * If no ID given, take user_id and try to generate one
59
     */
60
    public function __construct(
61
        $certificate_id = 0,
62
        $userId = 0,
63
        $sendNotification = false,
64
        $updateCertificateData = true,
65
        $pathToCertificate = ''
66
    ) {
67
        $this->table   = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
68
        $this->user_id = !empty($userId) ? (int) $userId : api_get_user_id();
69
70
        // Load legacy row if an ID is provided
71
        if (!empty($certificate_id)) {
72
            $certificate = $this->get($certificate_id);
73
            if (is_array($certificate) && !empty($certificate)) {
74
                $this->certificate_data = $certificate;
75
                $this->user_id = (int) $this->certificate_data['user_id'];
76
            }
77
        }
78
79
        if (empty($this->user_id)) {
80
            // No user context, nothing else to do.
81
            return;
82
        }
83
84
        $certRepo   = Container::getGradeBookCertificateRepository();
85
        $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
86
87
        // Try to preload an existing resource to avoid unnecessary work.
88
        try {
89
            $existing = $certRepo->getCertificateByUserId($categoryId === 0 ? null : $categoryId, $this->user_id);
90
            if ($existing && $existing->hasResourceNode()) {
91
                // Resource-first model: legacy path is not used anymore.
92
                $this->certification_user_path = 'resource://user_certificate';
93
                $this->html_file = '';
94
                $this->certificate_data['path_certificate'] = '';
95
                $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($existing);
96
            }
97
        } catch (\Throwable $e) {
98
            // Non-fatal; generation can still proceed if needed.
99
            error_log('[CERT::__construct] preload resource error: '.$e->getMessage());
100
        }
101
102
        // Keep original behavior: optionally generate on construct.
103
        if ($this->force_certificate_generation) {
104
            try {
105
                $this->generate(['certificate_path' => $pathToCertificate], $sendNotification);
106
                // Refresh in-memory HTML for PDF generation after generate().
107
                $refetched = $certRepo->getCertificateByUserId($categoryId === 0 ? null : $categoryId, $this->user_id);
108
                if ($refetched && $refetched->hasResourceNode()) {
109
                    $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($refetched);
110
                    $this->certification_user_path = 'resource://user_certificate';
111
                }
112
            } catch (\Throwable $e) {
113
                error_log('[CERT::__construct] generate-on-construct failed: '.$e->getMessage());
114
            }
115
        }
116
117
        // If still empty legacy path but we have a row, keep original fallback.
118
        if (
119
            isset($this->certificate_data) &&
120
            $this->certificate_data &&
121
            empty($this->certificate_data['path_certificate']) &&
122
            !$this->force_certificate_generation
123
        ) {
124
            try {
125
                $this->generate(['certificate_path' => $pathToCertificate], $sendNotification);
126
                $refetched = $certRepo->getCertificateByUserId($categoryId === 0 ? null : $categoryId, $this->user_id);
127
                if ($refetched && $refetched->hasResourceNode()) {
128
                    $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($refetched);
129
                    $this->certification_user_path = 'resource://user_certificate';
130
                }
131
            } catch (\Throwable $e) {
132
                error_log('[CERT::__construct] generate (no legacy path) failed: '.$e->getMessage());
133
            }
134
        }
135
136
        // Setting the qr and html variables
137
        if (
138
            isset($certificate_id) &&
139
            !empty($this->certification_user_path) &&
140
            isset($this->certificate_data['path_certificate']) &&
141
            !empty($this->certificate_data['path_certificate'])
142
        ) {
143
            // Legacy: path points to a file name; we only keep it for BC.
144
            $this->html_file = $this->certificate_data['path_certificate'];
145
        } else {
146
            if ('true' === api_get_setting('certificate.allow_general_certificate')) {
147
                // Guard: if a resource already exists, just populate file_content and exit.
148
                try {
149
                    $already = $certRepo->getCertificateByUserId(null, $this->user_id); // general certificate => null cat
150
                    if ($already && $already->hasResourceNode()) {
151
                        $this->certification_user_path = 'resource://user_certificate';
152
                        $this->certificate_data['path_certificate'] = '';
153
                        $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($already);
154
                        return; // Nothing else to do.
155
                    }
156
                } catch (\Throwable $e) {
157
                    error_log('[CERT::__construct] check-existing general cert error: '.$e->getMessage());
158
                }
159
160
                // General certificate
161
                // store as a Resource (resource_type = user_certificate) instead of PersonalFile
162
                $cert = null;
163
                try {
164
                    // Build HTML content (always available for PDF even if upsert fails).
165
                    $content = $this->generateCustomCertificate();
166
167
                    $hash     = hash('sha256', $this->user_id.$categoryId);
168
                    $fileName = $hash.'.html';
169
170
                    // upsertCertificateResource(catId, userId, score, htmlContent, pdfBinary?, legacyFileName?)
171
                    $cert = $certRepo->upsertCertificateResource(0, $this->user_id, 100.0, $content, null, $fileName);
172
173
                    // Keep legacy compatibility fields in DB if required
174
                    if ($updateCertificateData) {
175
                        $certRepo->registerUserInfoAboutCertificate(0, $this->user_id, 100.0, $fileName);
176
                    }
177
178
                    // Update in-memory fields for downstream consumers (PDF)
179
                    $this->certification_user_path = 'resource://user_certificate';
180
                    $this->certificate_data['path_certificate'] = $fileName;
181
182
                    // Ensure file_content is always available (avoid undefined index)
183
                    try {
184
                        $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($cert);
185
                    } catch (\Throwable $ignored) {
186
                        // Fallback: use raw generated HTML so PDF creation never crashes.
187
                        $this->certificate_data['file_content'] = $content;
188
                    }
189
190
                    // Optional: keep the legacy user-certificate metadata updated
191
                    $this->updateUserCertificateInfo(
192
                        0,
193
                        $this->user_id,
194
                        $fileName,
195
                        $updateCertificateData
196
                    );
197
                } catch (\Throwable $e) {
198
                    // Do not break the constructor; log and keep going
199
                    error_log('[CERT] general certificate upsert error: '.$e->getMessage());
200
                    // As a last resort, populate file_content with a minimal HTML to avoid PDF fatal errors
201
                    if (empty($this->certificate_data['file_content'])) {
202
                        $this->certificate_data['file_content'] = '<html><body><p></p></body></html>';
203
                    }
204
                }
205
            }
206
        }
207
    }
208
209
    /**
210
     * Deletes the current certificate object. This is generally triggered by
211
     * the teacher from the gradebook tool to re-generate the certificate because
212
     * the original version wa flawed.
213
     *
214
     * @param bool $force_delete
215
     *
216
     * @return bool
217
     */
218
    public function deleteCertificate(): bool
219
    {
220
        if (empty($this->certificate_data)) {
221
            return false;
222
        }
223
224
        $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
225
        $certRepo   = Container::getGradeBookCertificateRepository();
226
227
        try {
228
            $certRepo->deleteCertificateResource($this->certificate_data['user_id'], $categoryId);
229
230
            return true;
231
        } catch (\Throwable $e) {
232
            error_log('[CERTIFICATE::deleteCertificate] delete error: '.$e->getMessage());
233
            return false;
234
        }
235
    }
236
237
    /**
238
     * Generates (or updates) the user's certificate as a Resource.
239
     *
240
     * - Stores the HTML in a ResourceNode (resource_type = user_certificate or fallback handled at repository level).
241
     * - Fills $this->certificate_data['file_content'] with the HTML to avoid PDF errors.
242
     * - Keeps legacy DB info via registerUserInfoAboutCertificate() (no PersonalFile usage).
243
     *
244
     * @param array $params
245
     * @param bool  $sendNotification
246
     *
247
     * @return bool
248
     */
249
    public function generate($params = [], $sendNotification = false)
250
    {
251
        // Safe defaults
252
        $params = is_array($params) ? $params : [];
253
        $params['hide_print_button'] = isset($params['hide_print_button'])
254
            ? (bool) $params['hide_print_button']
255
            : false;
256
257
        $certRepo = Container::getGradeBookCertificateRepository();
258
259
        $categoryId = 0;
260
        $category   = null;
261
        $isCertificateAvailableInCategory = false;
262
263
        // If the certificate is linked to a Gradebook category, check availability
264
        if (isset($this->certificate_data['cat_id'])) {
265
            $categoryId = (int) $this->certificate_data['cat_id'];
266
267
            // Category::load() returns an array
268
            $myCategory = Category::load($categoryId);
269
270
            $repo = Container::getGradeBookCategoryRepository();
271
            /** @var \Chamilo\CoreBundle\Entity\GradebookCategory|null $category */
272
            $category = $repo->find($categoryId);
273
274
            if (!empty($categoryId) && !empty($myCategory) && isset($myCategory[0])) {
275
                $isCertificateAvailableInCategory = $myCategory[0]->is_certificate_available($this->user_id);
276
            }
277
        }
278
279
        // Path A: course/session-bound certificate
280
        if ($isCertificateAvailableInCategory && null !== $category) {
281
            // Course/session info
282
            $course     = $category->getCourse();
283
            $courseInfo = api_get_course_info($course->getCode());
284
            $courseId   = $courseInfo['real_id'];
285
            $sessionId  = $category->getSession() ? (int) $category->getSession()->getId() : 0;
286
287
            // Award related skill
288
            $skill = new SkillModel();
289
            $skill->addSkillToUser(
290
                $this->user_id,
291
                $category,
292
                $courseId,
293
                $sessionId
294
            );
295
296
            // Build certificate HTML and score
297
            $gb = GradebookUtils::get_user_certificate_content(
298
                $this->user_id,
299
                $course->getId(),
300
                $sessionId,
301
                false,
302
                $params['hide_print_button']
303
            );
304
305
            $html  = '';
306
            $score = 100.0;
307
308
            if (is_array($gb)) {
309
                $html  = isset($gb['content']) ? (string) $gb['content'] : '';
310
                $score = isset($gb['score']) ? (float) $gb['score'] : 100.0;
311
            } elseif (is_string($gb) && $gb !== '') {
312
                // Some custom implementations might return a raw string
313
                $html = $gb;
314
            }
315
316
            if ($html === '') {
317
                error_log('[CERT::generate] Empty HTML content for category certificate (cat='.$categoryId.', user='.$this->user_id.').');
318
                return false;
319
            }
320
321
            try {
322
                // Persist as Resource and register legacy info (no PersonalFile)
323
                $entity = $certRepo->upsertCertificateResource($categoryId, $this->user_id, $score, $html);
324
                $certRepo->registerUserInfoAboutCertificate($categoryId, $this->user_id, $score);
325
326
                // Ensure PDF flow has the HTML in memory
327
                $this->certificate_data['file_content']   = $html;
328
                $this->certificate_data['path_certificate'] = ''; // stored as resource, no legacy file path
329
330
                // Send notification if required (we have course context here)
331
                if ($sendNotification) {
332
                    $subject = get_lang('Certificate notification');
333
                    $message = nl2br(get_lang('((user_first_name)),'));
334
                    $htmlUrl = $certRepo->getResourceFileUrl($entity);
335
336
                    self::sendNotification(
337
                        $subject,
338
                        $message,
339
                        api_get_user_info($this->user_id),
340
                        $courseInfo,
341
                        [
342
                            'score_certificate' => $score,
343
                            'html_url'          => $htmlUrl,
344
                        ]
345
                    );
346
                }
347
348
                return true;
349
            } catch (\Throwable $e) {
350
                error_log('[CERT::generate] Upsert failed for category certificate (cat='.$categoryId.', user='.$this->user_id.'): '.$e->getMessage());
351
                return false;
352
            }
353
        }
354
355
        // Path B: general (portal-wide) certificate
356
        try {
357
            $html   = $this->generateCustomCertificate('');
358
            $score  = 100.0;
359
360
            if ($html === '') {
361
                error_log('[CERT::generate] Empty HTML content for general certificate (user='.$this->user_id.').');
362
                return false;
363
            }
364
365
            $entity = $certRepo->upsertCertificateResource(0, $this->user_id, $score, $html);
366
            $certRepo->registerUserInfoAboutCertificate(0, $this->user_id, $score);
367
368
            // Ensure PDF flow has the HTML in memory
369
            $this->certificate_data['file_content']     = $html;
370
            $this->certificate_data['path_certificate'] = ''; // stored as resource
371
372
            // No course context here, so we skip notification (sendNotification would fail its own checks)
373
            return true;
374
        } catch (\Throwable $e) {
375
            error_log('[CERT::generate] General certificate upsert failed (user='.$this->user_id.'): '.$e->getMessage());
376
            return false;
377
        }
378
    }
379
380
    /**
381
     * @return array
382
     */
383
    public static function notificationTags()
384
    {
385
        $tags = [
386
            '((course_title))',
387
            '((user_first_name))',
388
            '((user_last_name))',
389
            '((author_first_name))',
390
            '((author_last_name))',
391
            '((score))',
392
            '((portal_name))',
393
            '((certificate_link))',
394
        ];
395
396
        return $tags;
397
    }
398
399
    /**
400
     * @param string $subject
401
     * @param string $message
402
     * @param array  $userInfo
403
     * @param array  $courseInfo
404
     * @param array  $certificateInfo
405
     *
406
     * @return bool
407
     */
408
    public static function sendNotification(
409
        $subject,
410
        $message,
411
        $userInfo,
412
        $courseInfo,
413
        $certificateInfo
414
    ) {
415
        if (empty($userInfo) || empty($courseInfo)) {
416
            return false;
417
        }
418
419
        $currentUserInfo = api_get_user_info();
420
        $url = '';
421
422
        // Prefer resource URL if present
423
        if (!empty($certificateInfo['html_url'])) {
424
            $url = $certificateInfo['html_url'];
425
        } elseif (!empty($certificateInfo['path_certificate'])) {
426
            $hash = pathinfo($certificateInfo['path_certificate'], PATHINFO_FILENAME);
427
            $url = api_get_path(WEB_PATH) . 'certificates/' . $hash . '.html';
428
        }
429
        $link = Display::url($url, $url);
430
431
        $replace = [
432
            $courseInfo['title'],
433
            $userInfo['firstname'],
434
            $userInfo['lastname'],
435
            $currentUserInfo['firstname'],
436
            $currentUserInfo['lastname'],
437
            $certificateInfo['score_certificate'],
438
            api_get_setting('Institution'),
439
            $link,
440
        ];
441
442
        $message = str_replace(self::notificationTags(), $replace, $message);
443
        MessageManager::send_message(
444
            $userInfo['id'],
445
            $subject,
446
            $message,
447
            [],
448
            [],
449
            0,
450
            0,
451
            0,
452
            0,
453
            $currentUserInfo['id']
454
        );
455
    }
456
457
    /**
458
     * Update user info about certificate.
459
     *
460
     * @param int    $categoryId            category id
461
     * @param int    $user_id               user id
462
     * @param string $path_certificate      the path name of the certificate
463
     * @param bool   $updateCertificateData
464
     */
465
    public function updateUserCertificateInfo(
466
        $categoryId,
467
        $user_id,
468
        $path_certificate,
469
        $updateCertificateData = true
470
    ) {
471
        if (!$updateCertificateData) {
472
            return;
473
        }
474
        $certRepo = Container::getGradeBookCertificateRepository();
475
476
        $certRepo->registerUserInfoAboutCertificate(
477
            (int)$categoryId,
478
            (int)$user_id,
479
            (float)($this->certificate_data['score_certificate'] ?? 100.0),
480
            (string)$path_certificate
481
        );
482
    }
483
484
    /**
485
     * Check if the file was generated.
486
     *
487
     * @return bool
488
     */
489
    public function isHtmlFileGenerated()
490
    {
491
        if (empty($this->certification_user_path)) {
492
            return false;
493
        }
494
        if (!empty($this->certificate_data) &&
495
            isset($this->certificate_data['path_certificate']) &&
496
            !empty($this->certificate_data['path_certificate'])
497
        ) {
498
            return true;
499
        }
500
501
        return false;
502
    }
503
504
    /**
505
     * Generates a QR code for the certificate. The QR code embeds the text given.
506
     *
507
     * @param string $text Text to be added in the QR code
508
     * @param string $path file path of the image
509
     *
510
     * @return bool
511
     */
512
    public function generateQRImage($text, $path): bool
513
    {
514
        throw new \Exception('generateQRImage');
515
        if (!empty($text) && !empty($path)) {
0 ignored issues
show
Unused Code introduced by
IfNode is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
516
            $qrCode = new QrCode($text);
517
            //$qrCode->setEncoding('UTF-8');
518
            $qrCode->setSize(120);
519
            $qrCode->setMargin(5);
520
            /*$qrCode->setWriterByName('png');
521
            $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::MEDIUM());
522
            $qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]);
523
            $qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]);
524
            $qrCode->setValidateResult(false);
525
            $qrCode->writeFile($path);*/
526
527
            return true;
528
        }
529
530
        return false;
531
    }
532
533
    /**
534
     * Transforms certificate tags into text values. This function is very static
535
     * (it doesn't allow for much flexibility in terms of what tags are printed).
536
     *
537
     * @param array $array Contains two array entries: first are the headers,
538
     *                     second is an array of contents
539
     *
540
     * @return string The translated string
541
     */
542
    public function parseCertificateVariables($array)
543
    {
544
        $headers = $array[0];
545
        $content = $array[1];
546
        $final_content = [];
547
548
        if (!empty($content)) {
549
            foreach ($content as $key => $value) {
550
                $my_header = str_replace(['((', '))'], '', $headers[$key]);
551
                $final_content[$my_header] = $value;
552
            }
553
        }
554
555
        /* Certificate tags
556
         *
557
          0 => string '((user_firstname))' (length=18)
558
          1 => string '((user_lastname))' (length=17)
559
          2 => string '((gradebook_institution))' (length=25)
560
          3 => string '((gradebook_sitename))' (length=22)
561
          4 => string '((teacher_firstname))' (length=21)
562
          5 => string '((teacher_lastname))' (length=20)
563
          6 => string '((official_code))' (length=17)
564
          7 => string '((date_certificate))' (length=20)
565
          8 => string '((course_code))' (length=15)
566
          9 => string '((course_title))' (length=16)
567
          10 => string '((gradebook_grade))' (length=19)
568
          11 => string '((certificate_link))' (length=20)
569
          12 => string '((certificate_link_html))' (length=25)
570
          13 => string '((certificate_barcode))' (length=23)
571
         */
572
573
        $break_space = " \n\r ";
574
        $text =
575
            $final_content['gradebook_institution'].' - '.
576
            $final_content['gradebook_sitename'].' - '.
577
            get_lang('Certification').$break_space.
578
            get_lang('Learner').': '.$final_content['user_firstname'].' '.$final_content['user_lastname'].$break_space.
579
            get_lang('Trainer').': '.$final_content['teacher_firstname'].' '.$final_content['teacher_lastname'].$break_space.
580
            get_lang('Date').': '.$final_content['date_certificate'].$break_space.
581
            get_lang('Score').': '.$final_content['gradebook_grade'].$break_space.
582
            'URL'.': '.$final_content['certificate_link'];
583
584
        return $text;
585
    }
586
587
    /**
588
     * Check if the certificate is visible for the current user
589
     * If the global setting allow_public_certificates is set to 'false', no certificate can be printed.
590
     * If the global allow_public_certificates is set to 'true' and the course setting allow_public_certificates
591
     * is set to 0, no certificate *in this course* can be printed (for anonymous users).
592
     * Connected users can always print them.
593
     *
594
     * @return bool
595
     */
596
    public function isVisible()
597
    {
598
        if (!api_is_anonymous()) {
599
            return true;
600
        }
601
602
        if ('true' != api_get_setting('certificate.allow_public_certificates')) {
603
            // The "non-public" setting is set, so do not print
604
            return false;
605
        }
606
607
        if (!isset($this->certificate_data, $this->certificate_data['cat_id'])) {
608
            return false;
609
        }
610
611
        $gradeBook = new Gradebook();
612
        $gradeBookInfo = $gradeBook->get($this->certificate_data['cat_id']);
613
614
        if (empty($gradeBookInfo['course_code'])) {
615
            return false;
616
        }
617
618
        $setting = api_get_course_setting(
619
            'allow_public_certificates',
620
            api_get_course_info($gradeBookInfo['course_code'])
621
        );
622
623
        if (0 == $setting) {
624
            // Printing not allowed
625
            return false;
626
        }
627
628
        return true;
629
    }
630
631
    /**
632
     * Check if the certificate is available.
633
     *
634
     * @return bool
635
     */
636
    public function isAvailable()
637
    {
638
        $certRepo = Container::getGradeBookCertificateRepository();
639
640
        $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
641
642
        try {
643
            $entity = $certRepo->getCertificateByUserId(0 === $categoryId ? null : $categoryId, $this->user_id);
644
            if (!$entity || !$entity->hasResourceNode()) {
645
                return false;
646
            }
647
648
            $node = $entity->getResourceNode();
649
            return $node->hasResourceFile() && $node->getResourceFiles()->count() > 0;
650
        } catch (\Throwable $e) {
651
            error_log('[CERTIFICATE::isAvailable] check error: '.$e->getMessage());
652
            return false;
653
        }
654
    }
655
656
    /**
657
     * Shows the student's certificate (HTML file).
658
     */
659
    public function show()
660
    {
661
        $certRepo = Container::getGradeBookCertificateRepository();
662
        $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
663
664
        try {
665
            $entity = $certRepo->getCertificateByUserId(0 === $categoryId ? null : $categoryId, $this->user_id);
666
            if (!$entity || !$entity->hasResourceNode()) {
667
                api_not_allowed(true);
668
            }
669
670
            // Read HTML content from the Resource layer
671
            $certificateContent = '<!DOCTYPE html>';
672
            $certificateContent .= $certRepo->getResourceFileContent($entity);
673
            $certificateContent = str_replace(' media="screen"', '', $certificateContent);
674
675
            // Track “downloaded_at” (legacy extra fields)
676
            if ($this->user_id == api_get_user_id() &&
677
                !empty($this->certificate_data) &&
678
                isset($this->certificate_data['id'])
679
            ) {
680
                $certificateId = $this->certificate_data['id'];
681
                $extraFieldValue = new ExtraFieldValue('user_certificate');
682
                $value = $extraFieldValue->get_values_by_handler_and_field_variable(
683
                    $certificateId,
684
                    'downloaded_at'
685
                );
686
                if (empty($value)) {
687
                    $params = [
688
                        'item_id' => $this->certificate_data['id'],
689
                        'extra_downloaded_at' => api_get_utc_datetime(),
690
                    ];
691
                    $extraFieldValue->saveFieldValues($params);
692
                }
693
            }
694
695
            header('Content-Type: text/html; charset='.api_get_system_encoding());
696
            echo $certificateContent;
697
            return;
698
        } catch (\Throwable $e) {
699
            error_log('[CERTIFICATE::show] read error: '.$e->getMessage());
700
            api_not_allowed(true);
701
        }
702
    }
703
704
    /**
705
     * @return string
706
     */
707
    public function generateCustomCertificate(string $fileName = ''): string
708
    {
709
        $certificateRepo = Container::getGradeBookCertificateRepository();
710
        $certificateRepo->registerUserInfoAboutCertificate(0, $this->user_id, 100, $fileName);
711
712
        $userInfo = api_get_user_info($this->user_id);
713
        $extraFieldValue = new ExtraFieldValue('user');
714
        $value = $extraFieldValue->get_values_by_handler_and_field_variable($this->user_id, 'legal_accept');
715
        $termsValidationDate = '';
716
        if (isset($value) && !empty($value['value'])) {
717
            [$id, $id2, $termsValidationDate] = explode(':', $value['value']);
718
        }
719
720
        $sessions = SessionManager::get_sessions_by_user($this->user_id, false, true);
721
        $totalTimeInLearningPaths = 0;
722
        $sessionsApproved = [];
723
        $coursesApproved = [];
724
        $courseList = [];
725
726
        $gradeBookRepo = Container::getGradeBookCategoryRepository();
727
        if ($sessions) {
728
            foreach ($sessions as $session) {
729
                $allCoursesApproved = [];
730
                foreach ($session['courses'] as $course) {
731
                    $course = api_get_course_entity($course['real_id']);
732
                    $courseId = $course->getId();
733
                    /* @var GradebookCategory $category */
734
                    $category = $gradeBookRepo->findOneBy(['course' => $course, 'session' => $session['session_id']]);
735
736
                    if (null !== $category) {
737
                        $result = Category::userFinishedCourse(
738
                            $this->user_id,
739
                            $category,
740
                            true,
741
                            $courseId,
742
                            $session['session_id']
743
                        );
744
745
                        $lpList = new LearnpathList(
746
                            $this->user_id,
747
                            api_get_course_info_by_id($courseId),
748
                            $session['session_id']
749
                        );
750
                        $lpFlatList = $lpList->get_flat_list();
751
752
                        // Find time spent in LP
753
                        $timeSpent = Tracking::get_time_spent_in_lp(
754
                            $this->user_id,
755
                            $course,
756
                            !empty($lpFlatList) ? array_keys($lpFlatList) : [],
757
                            $session['session_id']
758
                        );
759
760
                        if (!isset($courseList[$courseId])) {
761
                            $courseList[$courseId]['approved'] = false;
762
                            $courseList[$courseId]['time_spent'] = 0;
763
                        }
764
765
                        if ($result) {
766
                            $courseList[$courseId]['approved'] = true;
767
                            $coursesApproved[$courseId] = $course->getTitle();
768
                            $allCoursesApproved[] = true;
769
                        }
770
                        $courseList[$courseId]['time_spent'] += $timeSpent;
771
                    }
772
                }
773
774
                if (count($allCoursesApproved) == count($session['courses'])) {
775
                    $sessionsApproved[] = $session;
776
                }
777
            }
778
        }
779
780
        $totalTimeInLearningPaths = 0;
781
        foreach ($courseList as $courseId => $courseData) {
782
            if (true === $courseData['approved']) {
783
                $totalTimeInLearningPaths += $courseData['time_spent'];
784
            }
785
        }
786
787
        $skill = new SkillModel();
788
        $skills = $skill->getStudentSkills($this->user_id, 2);
789
        $allowAll = ('true' === api_get_setting('skill.allow_teacher_access_student_skills'));
790
        $courseIdForSkills  = $allowAll ? 0 : 0;
791
        $sessionIdForSkills = $allowAll ? 0 : 0;
792
        $skillsTable = $skill->getUserSkillsTable(
793
            $this->user_id,
794
            $courseIdForSkills,
795
            $sessionIdForSkills,
796
            false
797
        );
798
799
        $timeInSeconds = Tracking::get_time_spent_on_the_platform(
800
            $this->user_id,
801
            'ever'
802
        );
803
        $time = api_time_to_hms($timeInSeconds);
804
805
        $tplContent = new Template(null, false, false, false, false, false);
806
807
        // variables for the default template
808
        $tplContent->assign('complete_name', $userInfo['complete_name']);
809
        $tplContent->assign('time_in_platform', $time);
810
        $tplContent->assign('certificate_generated_date', isset($myCertificate['created_at']) ? api_get_local_time($myCertificate['created_at']) : '');
811
        if (!empty($termsValidationDate)) {
812
            $termsValidationDate = api_get_local_time($termsValidationDate);
813
        }
814
        $tplContent->assign('terms_validation_date', $termsValidationDate);
815
816
        if (empty($totalTimeInLearningPaths)) {
817
            $totalTimeInLearningPaths = $timeInSeconds;
818
        }
819
820
        // Ofaj
821
        $tplContent->assign('time_in_platform_in_hours', round($timeInSeconds/3600, 1));
822
        $tplContent->assign(
823
            'certificate_generated_date_no_time',
824
            api_get_local_time(
825
                $myCertificate['created_at'] ?? null,
826
                null,
827
                null,
828
                false,
829
                false,
830
                false,
831
                'd-m-Y'
832
            )
833
        );
834
        $tplContent->assign(
835
            'terms_validation_date_no_time',
836
            api_get_local_time(
837
                $termsValidationDate,
838
                null,
839
                null,
840
                false,
841
                false,
842
                false,
843
                'd-m-Y'
844
            )
845
        );
846
        $tplContent->assign('skills', $skills);
847
        $tplContent->assign('skills_table_html', $skillsTable['table']);
848
        $tplContent->assign('skills_rows', $skillsTable['skills']);
849
        $tplContent->assign('sessions', $sessionsApproved);
850
        $tplContent->assign('courses', $coursesApproved);
851
        $tplContent->assign('time_spent_in_lps', api_time_to_hms($totalTimeInLearningPaths));
852
        $tplContent->assign('time_spent_in_lps_in_hours', round($totalTimeInLearningPaths/3600, 1));
853
854
        $layoutContent = $tplContent->get_template('gradebook/custom_certificate.html.twig');
855
        $content = $tplContent->fetch($layoutContent);
856
857
        return $content;
858
    }
859
860
    /**
861
     * Ofaj.
862
     */
863
    public function generatePdfFromCustomCertificate(): void
864
    {
865
        $orientation = api_get_setting('certificate.certificate_pdf_orientation');
866
867
        $params['orientation'] = 'landscape';
868
        if (!empty($orientation)) {
869
            $params['orientation'] = $orientation;
870
        }
871
872
        $params['left'] = 0;
873
        $params['right'] = 0;
874
        $params['top'] = 0;
875
        $params['bottom'] = 0;
876
        $page_format = 'landscape' == $params['orientation'] ? 'A4-L' : 'A4';
877
        $pdf = new PDF($page_format, $params['orientation'], $params);
878
879
        if (empty($this->certificate_data['file_content'])) {
880
            try {
881
                $certRepo   = Container::getGradeBookCertificateRepository();
882
                $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
883
                $entity     = $certRepo->getCertificateByUserId(0 === $categoryId ? null : $categoryId, $this->user_id);
884
                if ($entity && $entity->hasResourceNode()) {
885
                    $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($entity);
886
                }
887
            } catch (\Throwable $e) {
888
                error_log('[CERT::generatePdfFromCustomCertificate] fallback read error: '.$e->getMessage());
889
            }
890
        }
891
892
        $pdf->content_to_pdf(
893
            $this->certificate_data['file_content'],
894
            null,
895
            get_lang('Certificates'),
896
            api_get_course_id(),
897
            'D',
898
            false,
899
            null,
900
            false,
901
            true,
902
            true,
903
            true,
904
            true
905
        );
906
    }
907
908
    /**
909
     * @param int $userId
910
     *
911
     * @return array
912
     */
913
    public static function getCertificateByUser($userId)
914
    {
915
        $userId = (int) $userId;
916
        if (empty($userId)) {
917
            return [];
918
        }
919
920
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
921
        $sql = "SELECT * FROM $table
922
                WHERE user_id= $userId";
923
        $rs = Database::query($sql);
924
925
        return Database::store_result($rs, 'ASSOC');
926
    }
927
928
    /**
929
     * @param int $userId
930
     */
931
    public static function generateUserSkills($userId)
932
    {
933
        $controller = new IndexManager(get_lang('My courses'));
934
        $courseAndSessions = $controller->returnCoursesAndSessions($userId, true, null, true, false);
935
        $repo = Container::getGradeBookCategoryRepository();
936
        if (isset($courseAndSessions['courses']) && !empty($courseAndSessions['courses'])) {
937
            foreach ($courseAndSessions['courses'] as $course) {
938
                $category = $repo->findOneBy(['course' => $course['real_id']]);
939
                /*$cats = Category::load(
940
                    null,
941
                    null,
942
                    $course['code'],
943
                    null,
944
                    null,
945
                    null,
946
                    false
947
                );*/
948
                if (null !== $category) {
949
                    Category::generateUserCertificate($category, $userId);
950
                }
951
            }
952
        }
953
954
        if (isset($courseAndSessions['sessions']) && !empty($courseAndSessions['sessions'])) {
955
            foreach ($courseAndSessions['sessions'] as $sessionCategory) {
956
                if (isset($sessionCategory['sessions'])) {
957
                    foreach ($sessionCategory['sessions'] as $sessionData) {
958
                        if (!empty($sessionData['courses'])) {
959
                            $sessionId = $sessionData['session_id'];
960
                            foreach ($sessionData['courses'] as $courseData) {
961
                                /*$cats = Category:: load(
962
                                    null,
963
                                    null,
964
                                    $courseData['course_code'],
965
                                    null,
966
                                    null,
967
                                    $sessionId,
968
                                    false
969
                                );*/
970
971
                                $category = $repo->findOneBy(
972
                                    ['course' => $courseData['real_id'], 'session' => $sessionId]
973
                                );
974
                                if (null !== $category) {
975
                                    Category::generateUserCertificate($category, $userId);
976
                                }
977
                            }
978
                        }
979
                    }
980
                }
981
            }
982
        }
983
    }
984
}
985