Passed
Pull Request — master (#7119)
by
unknown
08:41
created

safeGenerateCertificateForCategory()   C

Complexity

Conditions 8
Paths 256

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 22
nc 256
nop 2
dl 0
loc 36
rs 6.9111
c 1
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\Skill;
7
use Chamilo\CoreBundle\Framework\Container;
8
9
/**
10
 * LP Final Item: muestra certificado y skills al completar el LP.
11
 */
12
$_in_course = true;
13
14
require_once __DIR__.'/../inc/global.inc.php';
15
16
$current_course_tool = TOOL_GRADEBOOK;
17
api_protect_course_script(true);
18
19
$courseCode = api_get_course_id();
20
$courseId   = api_get_course_int_id();
21
$userId     = api_get_user_id();
22
$sessionId  = api_get_session_id();
23
$id         = isset($_GET['id']) ? (int) $_GET['id'] : 0;
24
$lpId       = isset($_GET['lp_id']) ? (int) $_GET['lp_id'] : 0;
25
26
// This page can only be shown from inside a learning path
27
if (!$id && !$lpId) {
28
    echo Display::return_message(get_lang('The file was not found'));
29
    exit;
30
}
31
32
// Certificate and Skills Premium with Service check
33
$plugin  = BuyCoursesPlugin::create();
34
$checker = $plugin->isEnabled() && $plugin->get('include_services');
35
36
if ($checker) {
37
    $userServiceSale = $plugin->getServiceSales(
38
        $userId,
39
        BuyCoursesPlugin::SERVICE_STATUS_COMPLETED,
40
        BuyCoursesPlugin::SERVICE_TYPE_LP_FINAL_ITEM,
41
        $lpId
42
    );
43
44
    if (empty($userServiceSale)) {
45
        // Instance a new template : No page tittle, No header, No footer
46
        $tpl = new Template(null, false, false);
47
        $url = api_get_path(WEB_PLUGIN_PATH).'BuyCourses/src/service_catalog.php';
48
        $content = sprintf(
49
            Display::return_message(
50
                get_lang('If you want to get the certificate and/or skills associated with this course, you need to buy the certificate service. You can go to the services catalog by clicking this link: %s'),
51
                'normal',
52
                false
53
            ),
54
            '<a href="'.$url.'">'.$url.'</a>'
55
        );
56
        $tpl->assign('content', $content);
57
        $tpl->display_blank_template();
58
        exit;
59
    }
60
}
61
62
$lpEntity = api_get_lp_entity($lpId);
63
$lp       = new Learnpath($lpEntity, [], $userId);
64
65
$count              = $lp->getTotalItemsCountWithoutDirs();
66
$completed          = $lp->get_complete_items_count(true);
67
$currentItemId      = $lp->get_current_item_id();
68
$currentItem        = $lp->items[$currentItemId] ?? null;
69
$currentItemStatus  = $currentItem ? $currentItem->get_status() : 'not attempted';
70
71
$lpItemRepo   = Container::getLpItemRepository();
72
$isFinalThere = false;
73
$isFinalDone  = false;
74
$finalItem    = null;
75
76
try {
77
    $finalItem = $lpItemRepo->findOneBy(['lp' => $lpEntity, 'itemType' => TOOL_LP_FINAL_ITEM]);
78
    if ($finalItem) {
79
        $isFinalThere = true;
80
        $fid = $finalItem->getIid();
81
        if (isset($lp->items[$fid])) {
82
            $st = $lp->items[$fid]->get_status();
83
            $isFinalDone = in_array($st, ['completed','passed','succeeded'], true);
84
        }
85
    }
86
} catch (\Throwable $e) {
87
    error_log('[LP_FINAL] final_item lookup error: '.$e->getMessage());
88
}
89
$countAdj     = max(0, $count    - ($isFinalThere ? 1 : 0));
90
$completedAdj = max(0, $completed - ($isFinalDone  ? 1 : 0));
91
$diff         = $countAdj - $completedAdj;
92
$accessGranted = false;
93
if ($diff === 0 || ($diff === 1 && (('incomplete' === $currentItemStatus) || ('not attempted' === $currentItemStatus)))) {
94
    if ($lp->prerequisites_match($currentItemId)) {
95
        $accessGranted = true;
96
    }
97
}
98
$lp->save_last();
99
unset($lp, $currentItem);
100
101
if (!$accessGranted) {
102
    echo Display::return_message(
103
        get_lang('This learning object cannot display because the course prerequisites are not completed. This happens when a course imposes that you follow it step by step or get a minimum score in tests before you reach the next steps.'),
104
        'warning'
105
    );
106
    $finalHtml = '';
107
} else {
108
    $downloadBlock = '';
109
    $badgeBlock    = '';
110
    $gbRepo        = Container::getGradeBookCategoryRepository();
111
    $courseEntity  = api_get_course_entity();
112
    $sessionEntity = api_get_session_entity();
113
114
    // Resolve GradebookCategory using lp_item.ref when item_type = final_item.
115
    // We store the gradebook category id in c_lp_item.ref (string).
116
    $categoryIdFromRef = 0;
117
118
    if (!empty($finalItem) && method_exists($finalItem, 'getRef')) {
119
        try {
120
            $refRaw = trim((string) $finalItem->getRef());
121
            if ($refRaw !== '' && $refRaw !== '0') {
122
                $categoryIdFromRef = (int) $refRaw;
123
            }
124
        } catch (\Throwable $e) {
125
            error_log('[LP_FINAL] Unable to read lp_item.ref for final_item: '.$e->getMessage());
126
        }
127
    }
128
129
    /** @var GradebookCategory|null $gbCat */
130
    $gbCat = null;
131
132
    // 1) First, try the explicit category id stored in c_lp_item.ref.
133
    if ($categoryIdFromRef > 0) {
134
        $gbCat = $gbRepo->find($categoryIdFromRef);
135
136
        // Safety check: ensure the referenced category belongs to the same course/session context.
137
        if ($gbCat && $courseEntity) {
138
            $catCourse  = $gbCat->getCourse();
139
            $catSession = $gbCat->getSession();
140
141
            // If course does not match, discard this category and let the fallback logic handle it.
142
            if (!$catCourse || $catCourse->getId() !== $courseEntity->getId()) {
143
                $gbCat = null;
144
            } elseif ($sessionEntity) {
145
                // If we are in a session context, ensure the category session matches.
146
                if ($catSession && $catSession->getId() !== $sessionEntity->getId()) {
147
                    $gbCat = null;
148
                }
149
            }
150
        }
151
    }
152
153
    // 2) Fallback: keep legacy behaviour (root course/session category).
154
    if (!$gbCat && $courseEntity) {
155
        if ($sessionEntity) {
156
            $gbCat = $gbRepo->findOneBy([
157
                'course'  => $courseEntity,
158
                'session' => $sessionEntity,
159
            ]);
160
        }
161
162
        if (!$gbCat) {
163
            $gbCat = $gbRepo->findOneBy([
164
                'course'  => $courseEntity,
165
                'session' => null,
166
            ]);
167
        }
168
    }
169
170
    if ($gbCat && !api_is_allowed_to_edit() && !api_is_excluded_user_type()) {
171
        // Use legacy Category business object to generate certificate + skills
172
        // for this specific gradebook category.
173
        // NOTE: Category::generateUserCertificate() is expected to know how to
174
        // work with the Doctrine GradebookCategory entity.
175
        $certificate = Category::generateUserCertificate($gbCat, $userId);
176
        if (!empty($certificate)) {
177
            // Build the HTML panel to replace ((certificate)).
178
            $downloadBlock = Category::getDownloadCertificateBlock($certificate);
179
        }
180
181
        // Skills: Category::generateUserCertificate() already assigns skills
182
        // to the user for this course/session/category when enabled.
183
        // Here we just render the user's skills panel.
184
        $badgeBlock = generateBadgePanel($userId, $courseId, $sessionId);
185
    }
186
187
    // Replace ((certificate)) and ((skill)) tokens in the final-item document.
188
    $finalHtml = renderFinalItemDocument($id, $downloadBlock, $badgeBlock);
189
}
190
191
$tpl = new Template(null, false, false);
192
$tpl->assign('content', $finalHtml);
193
$tpl->display_blank_template();
194
195
/**
196
 * Generates/ensures the certificate via Doctrine repositories and returns minimal link data.
197
 */
198
function safeGenerateCertificateForCategory(GradebookCategory $category, int $userId): array
199
{
200
    $course   = $category->getCourse();
201
    $session  = $category->getSession();
202
    $courseId = $course ? $course->getId() : 0;
0 ignored issues
show
introduced by
$course is of type Chamilo\CoreBundle\Entity\Course, thus it always evaluated to true.
Loading history...
203
    $sessId   = $session ? $session->getId() : 0;
204
    $catId    = (int) $category->getId();
205
206
    // Build certificate content & score.
207
    $gb    = GradebookUtils::get_user_certificate_content($userId, $courseId, $sessId);
208
    $html  = (is_array($gb) && isset($gb['content'])) ? $gb['content'] : '';
209
    $score = isset($gb['score']) ? (float) $gb['score'] : 100.0;
210
211
    $certRepo = Container::getGradeBookCertificateRepository();
212
213
    $htmlUrl = '';
214
    $pdfUrl  = '';
215
    $cert    = null;
216
217
    try {
218
        // Store/refresh as Resource (controlled access; not shown in "My personal files").
219
        $cert = $certRepo->upsertCertificateResource($catId, $userId, $score, $html);
220
221
        // (Optional) keep metadata (created_at/score). Filename is not required anymore.
222
        $certRepo->registerUserInfoAboutCertificate($catId, $userId, $score);
223
224
        // Build URLs from the Resource layer.
225
        $htmlUrl = $certRepo->getResourceFileUrl($cert);
226
    } catch (\Throwable $e) {
227
        error_log('[LP_FINAL] register cert error: '.$e->getMessage());
228
    }
229
230
    return [
231
        'path_certificate' => $cert ? (string) ($cert->getPathCertificate() ?? '') : '',
0 ignored issues
show
introduced by
$cert is of type Chamilo\CoreBundle\Entity\GradebookCertificate, thus it always evaluated to true.
Loading history...
232
        'html_url'         => $htmlUrl,
233
        'pdf_url'          => $pdfUrl,
234
    ];
235
}
236
237
/**
238
 * Builds the certificate download/view HTML block (if available).
239
 */
240
function buildCertificateBlock(array $cert): string
241
{
242
    $htmlUrl = $cert['html_url'] ?? '';
243
    $pdfUrl  = $cert['pdf_url']  ?? '';
244
    if (!$htmlUrl && !$pdfUrl) {
245
        return '';
246
    }
247
248
    $downloadBtn = $pdfUrl
249
        ? Display::toolbarButton(get_lang('Download certificate in PDF'), $pdfUrl, 'file-pdf-box')
250
        : '';
251
252
    $viewBtn = $htmlUrl
253
        ? Display::url(get_lang('View certificate'), $htmlUrl, ['class' => 'btn btn-default'])
254
        : '';
255
256
    return "
257
        <div class='panel panel-default'>
258
            <div class='panel-body'>
259
                <h3 class='text-center'>".get_lang('You can now download your certificate by clicking here')."</h3>
260
                <div class='text-center'>{$downloadBtn} {$viewBtn}</div>
261
            </div>
262
        </div>
263
    ";
264
}
265
266
/**
267
 * Returns the user's skills panel HTML (empty if none).
268
 */
269
function generateBadgePanel(int $userId, int $courseId, int $sessionId = 0): string
270
{
271
    $em           = Database::getManager();
272
    $skillRelUser = new SkillRelUserModel();
273
    $userSkills   = $skillRelUser->getUserSkills($userId, $courseId, $sessionId);
274
    if (!$userSkills) {
275
        return '';
276
    }
277
278
    $items = '';
279
    foreach ($userSkills as $row) {
280
        $skill = $em->find(Skill::class, $row['skill_id']);
281
        if (!$skill) {
282
            continue;
283
        }
284
285
        $items .= "
286
            <div class='row'>
287
                <div class='col-md-2 col-xs-4'>
288
                    <div class='thumbnail'>
289
                        <img class='skill-badge-img' src='".SkillModel::getWebIconPath($skill)."' >
290
                    </div>
291
                </div>
292
                <div class='col-md-8 col-xs-8'>
293
                    <h5><b>".$skill->getTitle()."</b></h5>
294
                    ".$skill->getDescription()."
295
                </div>
296
                <div class='col-md-2 col-xs-12'>
297
                    <h5><b>".get_lang('Share with your friends')."</b></h5>
298
                    <a href='http://www.facebook.com/sharer.php?u=".api_get_path(WEB_PATH)."badge/".$skill->getId()."/user/".$userId."' target='_new'>
299
                        <em class='fa fa-facebook-square fa-3x text-info' aria-hidden='true'></em>
300
                    </a>
301
                    <a href='https://twitter.com/home?status=".sprintf(get_lang('I have achieved skill %s on %s'), '"'.$skill->getTitle().'"', api_get_setting('siteName')).' - '.api_get_path(WEB_PATH).'badge/'.$skill->getId().'/user/'.$userId."' target='_new'>
302
                        <em class='fa fa-twitter-square fa-3x text-light' aria-hidden='true'></em>
303
                    </a>
304
                </div>
305
            </div>";
306
    }
307
308
    if (!$items) {
309
        return '';
310
    }
311
312
    return "
313
        <div class='panel panel-default'>
314
            <div class='panel-body'>
315
                <h3 class='text-center'>".get_lang('Additionally, you have achieved the following skills')."</h3>
316
                {$items}
317
            </div>
318
        </div>
319
    ";
320
}
321
322
/**
323
 * Render the Learning Path final-item document.
324
 */
325
function renderFinalItemDocument(int $lpItemOrDocId, string $certificateBlock, string $badgeBlock): string
326
{
327
    $docRepo    = Container::getDocumentRepository();
328
    $lpItemRepo = Container::getLpItemRepository();
329
330
    $document = null;
331
332
    // First, try to use the id directly as a document iid.
333
    try {
334
        $document = $docRepo->find($lpItemOrDocId);
335
    } catch (\Throwable $e) {
336
        // Silence here, we will try the LP item fallback below.
337
    }
338
339
    // If not a document iid, try resolving from the LP item path.
340
    if (!$document) {
341
        try {
342
            $lpItem = $lpItemRepo->find($lpItemOrDocId);
343
            if ($lpItem) {
344
                // In our case, lp_item.path stores the document iid as string.
345
                $document = $docRepo->find((int) $lpItem->getPath());
346
            }
347
        } catch (\Throwable $e) {
348
            // As a last resort, fail quietly and return empty content.
349
        }
350
    }
351
352
    if (!$document) {
353
        return '';
354
    }
355
356
    try {
357
        $content = $docRepo->getResourceFileContent($document);
358
    } catch (\Throwable $e) {
359
        error_log('[LP_FINAL] read doc error: '.$e->getMessage());
360
        return '';
361
    }
362
363
    $hasCert  = str_contains($content, '((certificate))');
364
    $hasSkill = str_contains($content, '((skill))');
365
366
    if ($hasCert) {
367
        $content = str_replace('((certificate))', $certificateBlock, $content);
368
    }
369
    if ($hasSkill) {
370
        $content = str_replace('((skill))', $badgeBlock, $content);
371
    }
372
373
    return $content;
374
}
375