Skip to content

Commit 7a46d00

Browse files
committed
customcert user's has option to use new or old format. Unique code is being generated. This PR is made for #1 and #2
custom cert, updated the names of both code generation methods. Added view_user_cert page, a logged in user can see certificate if having a valid token and ecard code. This is being used from view_tokens.php page. customcert view certificate function is made. This is made for #212 Updated verify certificate URL. This PR is made for 221 Updated the verify URL for QR code.
1 parent 44e1ef4 commit 7a46d00

File tree

5 files changed

+153
-7
lines changed

5 files changed

+153
-7
lines changed

db/install.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
</KEYS>
6060
<INDEXES>
6161
<INDEX NAME="userid-customcertid" UNIQUE="false" FIELDS="userid, customcertid"/>
62-
<INDEX NAME="code" UNIQUE="false" FIELDS="code"/>
62+
<INDEX NAME="code" UNIQUE="true" FIELDS="code"/>
6363
</INDEXES>
6464
</TABLE>
6565
<TABLE NAME="customcert_pages" COMMENT="Stores each page of a custom cert">

db/upgrade.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,21 +298,23 @@ function xmldb_customcert_upgrade($oldversion) {
298298
upgrade_mod_savepoint(true, 2024042205, 'customcert');
299299
}
300300

301+
// Drop the unique index on 'code' and add a non-unique one.
301302
if ($oldversion < 2024042210) {
302303
$table = new xmldb_table('customcert_issues');
303-
$index = new xmldb_index('code', XMLDB_INDEX_UNIQUE, ['code']);
304304

305+
// Drop existing unique index if it exists.
306+
$index = new xmldb_index('code', XMLDB_INDEX_UNIQUE, ['code']);
305307
if ($dbman->index_exists($table, $index)) {
306308
$dbman->drop_index($table, $index);
307309
}
308310

311+
// Add non-unique index.
309312
$index = new xmldb_index('code', XMLDB_INDEX_NOTUNIQUE, ['code']);
310-
311313
if (!$dbman->index_exists($table, $index)) {
312314
$dbman->add_index($table, $index);
313315
}
314316

315-
// Update the plugin version in the database.
317+
// Save the upgrade step.
316318
upgrade_plugin_savepoint(true, 2024042210, 'mod', 'customcert');
317319
}
318320

element/qrcode/classes/element.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@ public function render($pdf, $preview, $user) {
158158
$context = \context::instance_by_id($issue->contextid);
159159

160160
$urlparams = [
161-
'code' => $code,
162-
'qrcode' => 1,
161+
'certId' => $code,
163162
];
164163

165164
// We only add the 'contextid' to the link if the site setting for verifying all certificates is off,
@@ -173,7 +172,7 @@ public function render($pdf, $preview, $user) {
173172
$urlparams['contextid'] = $issue->contextid;
174173
}
175174

176-
$qrcodeurl = new \moodle_url('/mod/customcert/verify_certificate.php', $urlparams);
175+
$qrcodeurl = new \moodle_url('https://app.pacificmedicaltraining.com/verify', $urlparams);
177176
$qrcodeurl = $qrcodeurl->out(false);
178177
}
179178

lib.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,63 @@ function mod_customcert_inplace_editable($itemtype, $itemid, $newvalue) {
436436
}
437437
}
438438

439+
// Prevent direct access to this file.
440+
defined('MOODLE_INTERNAL') || die();
441+
442+
/**
443+
* Generates a public URL for viewing a user's certificate (eCard).
444+
*
445+
* This function constructs a URL that allows public access to a certificate
446+
* without requiring authentication. It does so by generating a secure token
447+
* based on the certificate code.
448+
*
449+
* @param string $cert_code The unique code of the certificate.
450+
* @return string The generated public URL for the certificate.
451+
*/
452+
function generate_public_url_for_certificate(string $cert_code): string {
453+
global $CFG;
454+
455+
// Generate a security token for the certificate using a private function.
456+
$token = calculate_signature($cert_code);
457+
458+
// Construct and return the public URL to view the certificate.
459+
return $CFG->wwwroot . '/mod/customcert/view_user_cert.php?cert_code=' . urlencode($cert_code) . '&token=' . urlencode($token);
460+
}
461+
462+
/**
463+
* Generates a secure HMAC signature for a certificate.
464+
*
465+
* This function creates a unique signature for a certificate based on its code.
466+
* The signature is used as a security token to verify access to the certificate.
467+
* It prevents unauthorized access by ensuring that only valid certificates can
468+
* be accessed through a generated URL.
469+
*
470+
* The signature is generated using the HMAC (Hash-based Message Authentication Code)
471+
* method with SHA-256, ensuring strong security. It uses Moodle's `siteidentifier`
472+
* as the secret key, making it unique to each Moodle installation.
473+
*
474+
* @param string $cert_code The unique certificate code.
475+
* @return string The generated HMAC signature.
476+
*/
477+
function calculate_signature(string $cert_code): string {
478+
global $CFG;
479+
480+
// Define a namespaced message prefix to avoid signature collisions.
481+
$messagePrefix = 'mod_customcert:view_user_cert';
482+
483+
// Construct the message that will be signed.
484+
// This includes the prefix and the certificate code to create a unique hash.
485+
$message = $messagePrefix . '|' . $cert_code;
486+
487+
// Use Moodle's unique site identifier as the secret key for HMAC.
488+
// This ensures that signatures are installation-specific.
489+
$secret = $CFG->siteidentifier;
490+
491+
// Generate the HMAC hash using SHA-256.
492+
// This provides a cryptographic signature that is difficult to forge.
493+
return hash_hmac('sha256', $message, $secret);
494+
}
495+
439496
/**
440497
* Get icon mapping for font-awesome.
441498
*/

view_user_cert.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
// Include required Moodle configuration and custom certificate library.
3+
require_once(__DIR__ . '/../../config.php');
4+
require_once($CFG->dirroot . '/mod/customcert/lib.php');
5+
6+
// Set up the page context before processing any parameters.
7+
// This ensures that Moodle properly initializes the page and handles any errors gracefully.
8+
$context = context_system::instance();
9+
$PAGE->set_context($context);
10+
$PAGE->set_url('/mod/customcert/view_user_cert.php');
11+
$PAGE->set_title('View certificate');
12+
$PAGE->set_heading('View certificate');
13+
14+
/**
15+
* Displays an error message in a formatted Moodle page and exits.
16+
*
17+
* This function helps standardize error handling by rendering the page
18+
* properly and showing the error message in an alert box.
19+
*
20+
* @param string $message The error message to display.
21+
*/
22+
function display_error_page($message) {
23+
global $OUTPUT;
24+
25+
echo $OUTPUT->header(); // Display the page header.
26+
echo $OUTPUT->box($message, 'alert alert-danger'); // Display the error message in a styled box.
27+
echo $OUTPUT->footer(); // Display the page footer.
28+
exit; // Stop further execution.
29+
}
30+
31+
// Retrieve certificate code and verification token from URL parameters.
32+
// 'optional_param' is used instead of 'required_param' to avoid Moodle throwing an automatic error page.
33+
$cert_code = optional_param('cert_code', '', PARAM_ALPHANUMEXT);
34+
$token = optional_param('token', '', PARAM_ALPHANUMEXT);
35+
36+
// Ensure both required parameters are provided.
37+
if (empty($cert_code) || empty($token)) {
38+
display_error_page('Certificate code or verification token is missing. Please check the URL and try again.');
39+
}
40+
41+
// Validate the provided token by regenerating it using the expected algorithm.
42+
$expected_token = calculate_signature($cert_code);
43+
if ($token !== $expected_token) {
44+
display_error_page('The verification token is invalid for this certificate. Please check the URL and try again.');
45+
}
46+
47+
// Retrieve the certificate issue entry using the provided certificate code.
48+
// This helps fetch the associated user ID to verify ownership.
49+
$issue = $DB->get_record('customcert_issues', ['code' => $cert_code], '*');
50+
51+
if (!$issue) {
52+
display_error_page('The certificate with the provided code could not be found. Please verify the certificate code and try again.');
53+
}
54+
55+
// Fetch the certificate associated with the retrieved issue.
56+
// The certificate must be one of the recognized eCard types: 'Cognitive eCard' or 'Completion eCard'.
57+
$certificate = $DB->get_record_sql("
58+
SELECT * FROM {customcert}
59+
WHERE id = ? AND name IN ('Cognitive eCard', 'Completion eCard')
60+
", [$issue->customcertid]);
61+
62+
if (!$certificate) {
63+
display_error_page('The certificate type is not valid or does not exist. Please contact the site administrator for assistance.');
64+
}
65+
66+
// Retrieve the corresponding template for the fetched certificate.
67+
// The template defines the layout and content of the generated certificate.
68+
$template = $DB->get_record('customcert_templates', ['id' => $certificate->templateid]);
69+
if (!$template) {
70+
display_error_page('The certificate template could not be found. Please contact the site administrator for assistance.');
71+
}
72+
73+
try {
74+
// Convert the template record into a template object.
75+
// This object provides methods to generate and render the certificate.
76+
$template = new \mod_customcert\template($template);
77+
78+
// Generate and output the certificate PDF.
79+
// 'false' indicates that the PDF is displayed inline instead of being force-downloaded.
80+
// The second parameter ensures the certificate is generated for the correct user.
81+
$template->generate_pdf(false, $issue->userid);
82+
} catch (Exception $e) {
83+
// Catch any errors that may occur while generating the certificate PDF.
84+
display_error_page('There was an error generating the certificate PDF. Please try again later or contact support if the problem persists.');
85+
}
86+
87+
// Prevent further execution after rendering the certificate.
88+
exit;

0 commit comments

Comments
 (0)