Completed
Push — master ( 8bced1...d1b872 )
by
unknown
01:45 queued 36s
created

Certificate::isVisible()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 6
nop 0
dl 0
loc 33
rs 9.1111
c 0
b 0
f 0
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
     * Template strategy is centralized here:
241
     *  1) If the Gradebook category HAS a default template (document) => use it.
242
     *  2) If it DOES NOT => fall back to the portal "custom" template (legacy behavior).
243
     * In both cases we store a category-bound Resource (cat_id), so pages don't need to re-implement fallbacks.
244
     *
245
     * - Stores the HTML in a ResourceNode (resource_type = user_certificate).
246
     * - Fills $this->certificate_data['file_content'] with the HTML to avoid PDF errors.
247
     * - Keeps legacy DB info via registerUserInfoAboutCertificate() (no PersonalFile usage).
248
     *
249
     * @param array $params
250
     * @param bool  $sendNotification
251
     *
252
     * @return bool
253
     */
254
    public function generate($params = [], $sendNotification = false)
255
    {
256
        // Safe defaults
257
        $params = is_array($params) ? $params : [];
258
        $params['hide_print_button'] = isset($params['hide_print_button'])
259
            ? (bool) $params['hide_print_button']
260
            : false;
261
262
        // Repository (required).
263
        try {
264
            $certRepo = Container::getGradeBookCertificateRepository();
265
        } catch (\Throwable $e) {
266
            error_log('[CERT::generate] FATAL: cannot get GradeBookCertificateRepository: '.$e->getMessage());
267
            return false;
268
        }
269
        if (!$certRepo) {
0 ignored issues
show
introduced by
$certRepo is of type Chamilo\CoreBundle\Repos...okCertificateRepository, thus it always evaluated to true.
Loading history...
270
            error_log('[CERT::generate] FATAL: GradeBookCertificateRepository is NULL');
271
            return false;
272
        }
273
274
        $categoryId = 0;
275
        $category   = null;
276
        $isCertificateAvailableInCategory = false;
277
278
        // If the certificate is linked to a Gradebook category, check availability
279
        if (isset($this->certificate_data['cat_id'])) {
280
            $categoryId = (int) $this->certificate_data['cat_id'];
281
282
            // Category::load() returns an array
283
            $myCategory = Category::load($categoryId);
284
285
            try {
286
                $repo = Container::getGradeBookCategoryRepository();
287
                /** @var GradebookCategory|null $category */
288
                $category = $repo ? $repo->find($categoryId) : null;
0 ignored issues
show
introduced by
$repo is of type Chamilo\CoreBundle\Repos...eBookCategoryRepository, thus it always evaluated to true.
Loading history...
289
            } catch (\Throwable $e) {
290
                error_log('[CERT::generate] category repo fetch failed: '.$e->getMessage());
291
                $category = null;
292
            }
293
294
            if (!empty($categoryId) && !empty($myCategory) && isset($myCategory[0])) {
295
                $isCertificateAvailableInCategory = $myCategory[0]->is_certificate_available($this->user_id);
296
            }
297
        }
298
299
        // Path A: course/session-bound certificate (category context)
300
        if ($isCertificateAvailableInCategory && null !== $category) {
301
            // Course/session info
302
            $course     = $category->getCourse();
303
            $courseInfo = api_get_course_info($course->getCode());
304
            $courseId   = $courseInfo['real_id'];
305
            $sessionId  = $category->getSession() ? (int) $category->getSession()->getId() : 0;
306
307
            try {
308
                $skill = new SkillModel();
309
                $skill->addSkillToUser(
310
                    $this->user_id,
311
                    $category,
312
                    $courseId,
313
                    $sessionId
314
                );
315
            } catch (\Throwable $e) {
316
                error_log('[CERT::generate] addSkillToUser failed: '.$e->getMessage());
317
            }
318
319
            $categoryHasDefaultTemplate = false;
320
            $documentIdForLog = null;
321
            try {
322
                $doc = $category->getDocument();
323
                if ($doc !== null) {
324
                    $categoryHasDefaultTemplate = true;
325
                    try {
326
                        $documentIdForLog = method_exists($doc, 'getId') ? $doc->getId() : null;
327
                    } catch (\Throwable $ignored) {
328
                        $documentIdForLog = null;
329
                    }
330
                }
331
            } catch (\Throwable $e) {
332
                error_log('[CERT::generate] getDocument() failed (no default template): '.$e->getMessage());
333
                $categoryHasDefaultTemplate = false;
334
            }
335
336
            $gb = GradebookUtils::get_user_certificate_content(
337
                $this->user_id,
338
                $course->getId(),
339
                $sessionId,
340
                false,
341
                $params['hide_print_button']
342
            );
343
344
            $score = 100.0;
345
            if (is_array($gb) && isset($gb['score'])) {
346
                $score = (float)$gb['score'];
347
            }
348
349
            $html   = '';
350
            $source = '';
351
352
            if ($categoryHasDefaultTemplate) {
353
                if (is_array($gb) && !empty($gb['content'])) {
354
                    $html   = (string)$gb['content'];
355
                    $source = 'DEFAULT_TEMPLATE';
356
                } elseif (is_string($gb) && $gb !== '') {
357
                    $html   = $gb;
358
                    $source = 'DEFAULT_TEMPLATE';
359
                }
360
            } else {
361
                error_log(sprintf(
362
                    '[CERT::generate] course DEFAULT template NOT found. cat=%d user=%d -> fallback to CUSTOM',
363
                    (int)$categoryId,
364
                    (int)$this->user_id
365
                ));
366
            }
367
368
            if ($html === '') {
369
                $html   = $this->generateCustomCertificate('');
370
                $source = 'CUSTOM_TEMPLATE_FALLBACK';
371
            }
372
373
            if ($html === '') {
374
                error_log(sprintf(
375
                    '[CERT::generate] Empty HTML on category path. cat=%d user=%d',
376
                    (int)$categoryId,
377
                    (int)$this->user_id
378
                ));
379
                return false;
380
            }
381
382
            try {
383
                // Persist as Resource and register legacy info (no PersonalFile)
384
                $entity = $certRepo->upsertCertificateResource($categoryId, $this->user_id, $score, $html);
385
                $certRepo->registerUserInfoAboutCertificate($categoryId, $this->user_id, $score);
386
387
                // Ensure PDF flow has the HTML in memory
388
                $this->certificate_data['file_content']     = $html;
389
                $this->certificate_data['path_certificate'] = '';
390
391
                // Send notification if required (we have course context here)
392
                if ($sendNotification) {
393
                    $subject = get_lang('Certificate notification');
394
                    $message = nl2br(get_lang('((user_first_name)),'));
395
                    $htmlUrl = '';
396
                    try {
397
                        $htmlUrl = $certRepo->getResourceFileUrl($entity);
398
                    } catch (\Throwable $e) {
399
                        error_log('[CERT::generate] getResourceFileUrl failed for notification: '.$e->getMessage());
400
                    }
401
402
                    self::sendNotification(
403
                        $subject,
404
                        $message,
405
                        api_get_user_info($this->user_id),
406
                        $courseInfo,
407
                        [
408
                            'score_certificate' => $score,
409
                            'html_url'          => $htmlUrl,
410
                        ]
411
                    );
412
                }
413
414
                return true;
415
            } catch (\Throwable $e) {
416
                error_log(sprintf(
417
                    '[CERT::generate] Upsert FAILED (course). cat=%d user=%d err=%s',
418
                    (int)$categoryId,
419
                    (int)$this->user_id,
420
                    $e->getMessage()
421
                ));
422
                return false;
423
            }
424
        }
425
426
        // Path B: general (portal-wide) certificate
427
        try {
428
            $html  = $this->generateCustomCertificate('');
429
            $score = 100.0;
430
431
            if ($html === '') {
432
                error_log(sprintf(
433
                    '[CERT::generate] Empty HTML on general path. user=%d',
434
                    (int)$this->user_id
435
                ));
436
                return false;
437
            }
438
439
            $entity = $certRepo->upsertCertificateResource(0, $this->user_id, $score, $html);
440
            $certRepo->registerUserInfoAboutCertificate(0, $this->user_id, $score);
441
442
            $this->certificate_data['file_content']     = $html;
443
            $this->certificate_data['path_certificate'] = ''; // resource
444
445
            return true;
446
        } catch (\Throwable $e) {
447
            error_log(sprintf(
448
                '[CERT::generate] Upsert FAILED (general). user=%d err=%s',
449
                (int)$this->user_id,
450
                $e->getMessage()
451
            ));
452
            return false;
453
        }
454
    }
455
456
    /**
457
     * @return array
458
     */
459
    public static function notificationTags()
460
    {
461
        $tags = [
462
            '((course_title))',
463
            '((user_first_name))',
464
            '((user_last_name))',
465
            '((author_first_name))',
466
            '((author_last_name))',
467
            '((score))',
468
            '((portal_name))',
469
            '((certificate_link))',
470
        ];
471
472
        return $tags;
473
    }
474
475
    /**
476
     * @param string $subject
477
     * @param string $message
478
     * @param array  $userInfo
479
     * @param array  $courseInfo
480
     * @param array  $certificateInfo
481
     *
482
     * @return bool
483
     */
484
    public static function sendNotification(
485
        $subject,
486
        $message,
487
        $userInfo,
488
        $courseInfo,
489
        $certificateInfo
490
    ) {
491
        if (empty($userInfo) || empty($courseInfo)) {
492
            return false;
493
        }
494
495
        $currentUserInfo = api_get_user_info();
496
        $url = '';
497
498
        // Prefer resource URL if present
499
        if (!empty($certificateInfo['html_url'])) {
500
            $url = $certificateInfo['html_url'];
501
        } elseif (!empty($certificateInfo['path_certificate'])) {
502
            $hash = pathinfo($certificateInfo['path_certificate'], PATHINFO_FILENAME);
503
            $url = api_get_path(WEB_PATH) . 'certificates/' . $hash . '.html';
504
        }
505
        $link = Display::url($url, $url);
506
507
        $replace = [
508
            $courseInfo['title'],
509
            $userInfo['firstname'],
510
            $userInfo['lastname'],
511
            $currentUserInfo['firstname'],
512
            $currentUserInfo['lastname'],
513
            $certificateInfo['score_certificate'],
514
            api_get_setting('Institution'),
515
            $link,
516
        ];
517
518
        $message = str_replace(self::notificationTags(), $replace, $message);
519
        MessageManager::send_message(
520
            $userInfo['id'],
521
            $subject,
522
            $message,
523
            [],
524
            [],
525
            0,
526
            0,
527
            0,
528
            0,
529
            $currentUserInfo['id']
530
        );
531
    }
532
533
    /**
534
     * Update user info about certificate.
535
     *
536
     * @param int    $categoryId            category id
537
     * @param int    $user_id               user id
538
     * @param string $path_certificate      the path name of the certificate
539
     * @param bool   $updateCertificateData
540
     */
541
    public function updateUserCertificateInfo(
542
        $categoryId,
543
        $user_id,
544
        $path_certificate,
545
        $updateCertificateData = true
546
    ) {
547
        if (!$updateCertificateData) {
548
            return;
549
        }
550
        $certRepo = Container::getGradeBookCertificateRepository();
551
552
        $certRepo->registerUserInfoAboutCertificate(
553
            (int)$categoryId,
554
            (int)$user_id,
555
            (float)($this->certificate_data['score_certificate'] ?? 100.0),
556
            (string)$path_certificate
557
        );
558
    }
559
560
    /**
561
     * Check if the file was generated.
562
     *
563
     * @return bool
564
     */
565
    public function isHtmlFileGenerated()
566
    {
567
        if (empty($this->certification_user_path)) {
568
            return false;
569
        }
570
        if (!empty($this->certificate_data) &&
571
            isset($this->certificate_data['path_certificate']) &&
572
            !empty($this->certificate_data['path_certificate'])
573
        ) {
574
            return true;
575
        }
576
577
        return false;
578
    }
579
580
    /**
581
     * Generates a QR code for the certificate. The QR code embeds the text given.
582
     *
583
     * @param string $text Text to be added in the QR code
584
     * @param string $path file path of the image
585
     *
586
     * @return bool
587
     */
588
    public function generateQRImage($text, $path): bool
589
    {
590
        throw new \Exception('generateQRImage');
591
        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...
592
            $qrCode = new QrCode($text);
593
            //$qrCode->setEncoding('UTF-8');
594
            $qrCode->setSize(120);
595
            $qrCode->setMargin(5);
596
            /*$qrCode->setWriterByName('png');
597
            $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::MEDIUM());
598
            $qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]);
599
            $qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]);
600
            $qrCode->setValidateResult(false);
601
            $qrCode->writeFile($path);*/
602
603
            return true;
604
        }
605
606
        return false;
607
    }
608
609
    /**
610
     * Transforms certificate tags into text values. This function is very static
611
     * (it doesn't allow for much flexibility in terms of what tags are printed).
612
     *
613
     * @param array $array Contains two array entries: first are the headers,
614
     *                     second is an array of contents
615
     *
616
     * @return string The translated string
617
     */
618
    public function parseCertificateVariables($array)
619
    {
620
        $headers = $array[0];
621
        $content = $array[1];
622
        $final_content = [];
623
624
        if (!empty($content)) {
625
            foreach ($content as $key => $value) {
626
                $my_header = str_replace(['((', '))'], '', $headers[$key]);
627
                $final_content[$my_header] = $value;
628
            }
629
        }
630
631
        /* Certificate tags
632
         *
633
          0 => string '((user_firstname))' (length=18)
634
          1 => string '((user_lastname))' (length=17)
635
          2 => string '((gradebook_institution))' (length=25)
636
          3 => string '((gradebook_sitename))' (length=22)
637
          4 => string '((teacher_firstname))' (length=21)
638
          5 => string '((teacher_lastname))' (length=20)
639
          6 => string '((official_code))' (length=17)
640
          7 => string '((date_certificate))' (length=20)
641
          8 => string '((course_code))' (length=15)
642
          9 => string '((course_title))' (length=16)
643
          10 => string '((gradebook_grade))' (length=19)
644
          11 => string '((certificate_link))' (length=20)
645
          12 => string '((certificate_link_html))' (length=25)
646
          13 => string '((certificate_barcode))' (length=23)
647
         */
648
649
        $break_space = " \n\r ";
650
        $text =
651
            $final_content['gradebook_institution'].' - '.
652
            $final_content['gradebook_sitename'].' - '.
653
            get_lang('Certification').$break_space.
654
            get_lang('Learner').': '.$final_content['user_firstname'].' '.$final_content['user_lastname'].$break_space.
655
            get_lang('Trainer').': '.$final_content['teacher_firstname'].' '.$final_content['teacher_lastname'].$break_space.
656
            get_lang('Date').': '.$final_content['date_certificate'].$break_space.
657
            get_lang('Score').': '.$final_content['gradebook_grade'].$break_space.
658
            'URL'.': '.$final_content['certificate_link'];
659
660
        return $text;
661
    }
662
663
    /**
664
     * Check if the certificate is visible for the current user
665
     * If the global setting allow_public_certificates is set to 'false', no certificate can be printed.
666
     * If the global allow_public_certificates is set to 'true' and the course setting allow_public_certificates
667
     * is set to 0, no certificate *in this course* can be printed (for anonymous users).
668
     * Connected users can always print them.
669
     *
670
     * @return bool
671
     */
672
    public function isVisible()
673
    {
674
        if (!api_is_anonymous()) {
675
            return true;
676
        }
677
678
        if ('true' != api_get_setting('certificate.allow_public_certificates')) {
679
            // The "non-public" setting is set, so do not print
680
            return false;
681
        }
682
683
        if (!isset($this->certificate_data, $this->certificate_data['cat_id'])) {
684
            return false;
685
        }
686
687
        $gradeBook = new Gradebook();
688
        $gradeBookInfo = $gradeBook->get($this->certificate_data['cat_id']);
689
690
        if (empty($gradeBookInfo['course_code'])) {
691
            return false;
692
        }
693
694
        $setting = api_get_course_setting(
695
            'allow_public_certificates',
696
            api_get_course_info($gradeBookInfo['course_code'])
697
        );
698
699
        if (0 == $setting) {
700
            // Printing not allowed
701
            return false;
702
        }
703
704
        return true;
705
    }
706
707
    /**
708
     * Check if the certificate is available.
709
     *
710
     * @return bool
711
     */
712
    public function isAvailable()
713
    {
714
        $certRepo = Container::getGradeBookCertificateRepository();
715
716
        $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
717
718
        try {
719
            $entity = $certRepo->getCertificateByUserId(0 === $categoryId ? null : $categoryId, $this->user_id);
720
            if (!$entity || !$entity->hasResourceNode()) {
721
                return false;
722
            }
723
724
            $node = $entity->getResourceNode();
725
            return $node->hasResourceFile() && $node->getResourceFiles()->count() > 0;
726
        } catch (\Throwable $e) {
727
            error_log('[CERTIFICATE::isAvailable] check error: '.$e->getMessage());
728
            return false;
729
        }
730
    }
731
732
    /**
733
     * Shows the student's certificate (HTML file).
734
     */
735
    public function show()
736
    {
737
        $certRepo = Container::getGradeBookCertificateRepository();
738
        $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
739
740
        try {
741
            $entity = $certRepo->getCertificateByUserId(0 === $categoryId ? null : $categoryId, $this->user_id);
742
            if (!$entity || !$entity->hasResourceNode()) {
743
                api_not_allowed(true);
744
            }
745
746
            // Read HTML content from the Resource layer
747
            $certificateContent = '<!DOCTYPE html>';
748
            $certificateContent .= $certRepo->getResourceFileContent($entity);
749
            $certificateContent = str_replace(' media="screen"', '', $certificateContent);
750
751
            // Track “downloaded_at” (legacy extra fields)
752
            if ($this->user_id == api_get_user_id() &&
753
                !empty($this->certificate_data) &&
754
                isset($this->certificate_data['id'])
755
            ) {
756
                $certificateId = $this->certificate_data['id'];
757
                $extraFieldValue = new ExtraFieldValue('user_certificate');
758
                $value = $extraFieldValue->get_values_by_handler_and_field_variable(
759
                    $certificateId,
760
                    'downloaded_at'
761
                );
762
                if (empty($value)) {
763
                    $params = [
764
                        'item_id' => $this->certificate_data['id'],
765
                        'extra_downloaded_at' => api_get_utc_datetime(),
766
                    ];
767
                    $extraFieldValue->saveFieldValues($params);
768
                }
769
            }
770
771
            header('Content-Type: text/html; charset='.api_get_system_encoding());
772
            echo $certificateContent;
773
            return;
774
        } catch (\Throwable $e) {
775
            error_log('[CERTIFICATE::show] read error: '.$e->getMessage());
776
            api_not_allowed(true);
777
        }
778
    }
779
780
    /**
781
     * @return string
782
     */
783
    public function generateCustomCertificate(string $fileName = ''): string
784
    {
785
        $certificateRepo = Container::getGradeBookCertificateRepository();
786
        $certificateRepo->registerUserInfoAboutCertificate(0, $this->user_id, 100, $fileName);
787
788
        $userInfo = api_get_user_info($this->user_id);
789
        $extraFieldValue = new ExtraFieldValue('user');
790
        $value = $extraFieldValue->get_values_by_handler_and_field_variable($this->user_id, 'legal_accept');
791
        $termsValidationDate = '';
792
        if (isset($value) && !empty($value['value'])) {
793
            [$id, $id2, $termsValidationDate] = explode(':', $value['value']);
794
        }
795
796
        $sessions = SessionManager::get_sessions_by_user($this->user_id, false, true);
797
        $totalTimeInLearningPaths = 0;
798
        $sessionsApproved = [];
799
        $coursesApproved = [];
800
        $courseList = [];
801
802
        $gradeBookRepo = Container::getGradeBookCategoryRepository();
803
        if ($sessions) {
804
            foreach ($sessions as $session) {
805
                $allCoursesApproved = [];
806
                foreach ($session['courses'] as $course) {
807
                    $course = api_get_course_entity($course['real_id']);
808
                    $courseId = $course->getId();
809
                    /* @var GradebookCategory $category */
810
                    $category = $gradeBookRepo->findOneBy(['course' => $course, 'session' => $session['session_id']]);
811
812
                    if (null !== $category) {
813
                        $result = Category::userFinishedCourse(
814
                            $this->user_id,
815
                            $category,
816
                            true,
817
                            $courseId,
818
                            $session['session_id']
819
                        );
820
821
                        $lpList = new LearnpathList(
822
                            $this->user_id,
823
                            api_get_course_info_by_id($courseId),
824
                            $session['session_id']
825
                        );
826
                        $lpFlatList = $lpList->get_flat_list();
827
828
                        // Find time spent in LP
829
                        $timeSpent = Tracking::get_time_spent_in_lp(
830
                            $this->user_id,
831
                            $course,
832
                            !empty($lpFlatList) ? array_keys($lpFlatList) : [],
833
                            $session['session_id']
834
                        );
835
836
                        if (!isset($courseList[$courseId])) {
837
                            $courseList[$courseId]['approved'] = false;
838
                            $courseList[$courseId]['time_spent'] = 0;
839
                        }
840
841
                        if ($result) {
842
                            $courseList[$courseId]['approved'] = true;
843
                            $coursesApproved[$courseId] = $course->getTitle();
844
                            $allCoursesApproved[] = true;
845
                        }
846
                        $courseList[$courseId]['time_spent'] += $timeSpent;
847
                    }
848
                }
849
850
                if (count($allCoursesApproved) == count($session['courses'])) {
851
                    $sessionsApproved[] = $session;
852
                }
853
            }
854
        }
855
856
        $totalTimeInLearningPaths = 0;
857
        foreach ($courseList as $courseId => $courseData) {
858
            if (true === $courseData['approved']) {
859
                $totalTimeInLearningPaths += $courseData['time_spent'];
860
            }
861
        }
862
863
        $skill = new SkillModel();
864
        $skills = $skill->getStudentSkills($this->user_id, 2);
865
        $allowAll = ('true' === api_get_setting('skill.allow_teacher_access_student_skills'));
866
        $courseIdForSkills  = $allowAll ? 0 : 0;
867
        $sessionIdForSkills = $allowAll ? 0 : 0;
868
        $skillsTable = $skill->getUserSkillsTable(
869
            $this->user_id,
870
            $courseIdForSkills,
871
            $sessionIdForSkills,
872
            false
873
        );
874
875
        $timeInSeconds = Tracking::get_time_spent_on_the_platform(
876
            $this->user_id,
877
            'ever'
878
        );
879
        $time = api_time_to_hms($timeInSeconds);
880
881
        $tplContent = new Template(null, false, false, false, false, false);
882
883
        // variables for the default template
884
        $tplContent->assign('complete_name', $userInfo['complete_name']);
885
        $tplContent->assign('time_in_platform', $time);
886
        $tplContent->assign('certificate_generated_date', isset($myCertificate['created_at']) ? api_get_local_time($myCertificate['created_at']) : '');
887
        if (!empty($termsValidationDate)) {
888
            $termsValidationDate = api_get_local_time($termsValidationDate);
889
        }
890
        $tplContent->assign('terms_validation_date', $termsValidationDate);
891
892
        if (empty($totalTimeInLearningPaths)) {
893
            $totalTimeInLearningPaths = $timeInSeconds;
894
        }
895
896
        // Ofaj
897
        $tplContent->assign('time_in_platform_in_hours', round($timeInSeconds/3600, 1));
898
        $tplContent->assign(
899
            'certificate_generated_date_no_time',
900
            api_get_local_time(
901
                $myCertificate['created_at'] ?? null,
902
                null,
903
                null,
904
                false,
905
                false,
906
                false,
907
                'd-m-Y'
908
            )
909
        );
910
        $tplContent->assign(
911
            'terms_validation_date_no_time',
912
            api_get_local_time(
913
                $termsValidationDate,
914
                null,
915
                null,
916
                false,
917
                false,
918
                false,
919
                'd-m-Y'
920
            )
921
        );
922
        $tplContent->assign('skills', $skills);
923
        $tplContent->assign('skills_table_html', $skillsTable['table']);
924
        $tplContent->assign('skills_rows', $skillsTable['skills']);
925
        $tplContent->assign('sessions', $sessionsApproved);
926
        $tplContent->assign('courses', $coursesApproved);
927
        $tplContent->assign('time_spent_in_lps', api_time_to_hms($totalTimeInLearningPaths));
928
        $tplContent->assign('time_spent_in_lps_in_hours', round($totalTimeInLearningPaths/3600, 1));
929
930
        $layoutContent = $tplContent->get_template('gradebook/custom_certificate.html.twig');
931
        $content = $tplContent->fetch($layoutContent);
932
933
        return $content;
934
    }
935
936
    /**
937
     * Ofaj.
938
     */
939
    public function generatePdfFromCustomCertificate(): void
940
    {
941
        $orientation = api_get_setting('certificate.certificate_pdf_orientation');
942
943
        $params['orientation'] = 'landscape';
944
        if (!empty($orientation)) {
945
            $params['orientation'] = $orientation;
946
        }
947
948
        $params['left'] = 0;
949
        $params['right'] = 0;
950
        $params['top'] = 0;
951
        $params['bottom'] = 0;
952
        $page_format = 'landscape' == $params['orientation'] ? 'A4-L' : 'A4';
953
        $pdf = new PDF($page_format, $params['orientation'], $params);
954
955
        // Safety: ensure HTML content is present; fetch from Resource if needed.
956
        if (empty($this->certificate_data['file_content'])) {
957
            try {
958
                $certRepo   = Container::getGradeBookCertificateRepository();
959
                $categoryId = isset($this->certificate_data['cat_id']) ? (int) $this->certificate_data['cat_id'] : 0;
960
                $entity     = $certRepo->getCertificateByUserId(0 === $categoryId ? null : $categoryId, $this->user_id);
961
                if ($entity && $entity->hasResourceNode()) {
962
                    $this->certificate_data['file_content'] = $certRepo->getResourceFileContent($entity);
963
                }
964
            } catch (\Throwable $e) {
965
                error_log('[CERT::generatePdfFromCustomCertificate] fallback read error: '.$e->getMessage());
966
            }
967
        }
968
969
        $pdf->content_to_pdf(
970
            $this->certificate_data['file_content'],
971
            null,
972
            get_lang('Certificates'),
973
            api_get_course_id(),
974
            'D',
975
            false,
976
            null,
977
            false,
978
            true,
979
            true,
980
            true,
981
            true
982
        );
983
    }
984
985
    /**
986
     * @param int $userId
987
     *
988
     * @return array
989
     */
990
    public static function getCertificateByUser($userId)
991
    {
992
        $userId = (int) $userId;
993
        if (empty($userId)) {
994
            return [];
995
        }
996
997
        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
998
        $sql = "SELECT * FROM $table
999
                WHERE user_id= $userId";
1000
        $rs = Database::query($sql);
1001
1002
        return Database::store_result($rs, 'ASSOC');
1003
    }
1004
1005
    /**
1006
     * @param int $userId
1007
     */
1008
    public static function generateUserSkills($userId)
1009
    {
1010
        $controller = new IndexManager(get_lang('My courses'));
1011
        $courseAndSessions = $controller->returnCoursesAndSessions($userId, true, null, true, false);
1012
        $repo = Container::getGradeBookCategoryRepository();
1013
        if (isset($courseAndSessions['courses']) && !empty($courseAndSessions['courses'])) {
1014
            foreach ($courseAndSessions['courses'] as $course) {
1015
                $category = $repo->findOneBy(['course' => $course['real_id']]);
1016
                /*$cats = Category::load(
1017
                    null,
1018
                    null,
1019
                    $course['code'],
1020
                    null,
1021
                    null,
1022
                    null,
1023
                    false
1024
                );*/
1025
                if (null !== $category) {
1026
                    Category::generateUserCertificate($category, $userId);
1027
                }
1028
            }
1029
        }
1030
1031
        if (isset($courseAndSessions['sessions']) && !empty($courseAndSessions['sessions'])) {
1032
            foreach ($courseAndSessions['sessions'] as $sessionCategory) {
1033
                if (isset($sessionCategory['sessions'])) {
1034
                    foreach ($sessionCategory['sessions'] as $sessionData) {
1035
                        if (!empty($sessionData['courses'])) {
1036
                            $sessionId = $sessionData['session_id'];
1037
                            foreach ($sessionData['courses'] as $courseData) {
1038
                                /*$cats = Category:: load(
1039
                                    null,
1040
                                    null,
1041
                                    $courseData['course_code'],
1042
                                    null,
1043
                                    null,
1044
                                    $sessionId,
1045
                                    false
1046
                                );*/
1047
1048
                                $category = $repo->findOneBy(
1049
                                    ['course' => $courseData['real_id'], 'session' => $sessionId]
1050
                                );
1051
                                if (null !== $category) {
1052
                                    Category::generateUserCertificate($category, $userId);
1053
                                }
1054
                            }
1055
                        }
1056
                    }
1057
                }
1058
            }
1059
        }
1060
    }
1061
}
1062