Skip to content

Commit 374ad02

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 db37088 commit 374ad02

File tree

8 files changed

+213
-14
lines changed

8 files changed

+213
-14
lines changed

classes/certificate.php

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -538,14 +538,25 @@ public static function issue_certificate($certificateid, $userid) {
538538
return $DB->insert_record('customcert_issues', $issue);
539539
}
540540

541-
/**
542-
* Generates a 10-digit code of random letters and numbers.
543-
*
544-
* @return string
545-
*/
546541
public static function generate_code() {
547542
global $DB;
548-
543+
544+
// Get the user's selected method from settings
545+
$method = get_config('customcert', 'codegenerationmethod');
546+
547+
// If the upper/lower/digits is selected (0), use the upper/lower/digits code generation method
548+
if ($method == 0) {
549+
return self::generate_code_upper_lower_digits();
550+
}
551+
552+
// Otherwise, use the digits with hyphens method (1)
553+
return self::generate_code_digits_with_hyphens();
554+
}
555+
556+
// Upper/lower/digits random string
557+
private static function generate_code_upper_lower_digits() {
558+
global $DB;
559+
549560
$uniquecodefound = false;
550561
$code = random_string(10);
551562
while (!$uniquecodefound) {
@@ -555,7 +566,34 @@ public static function generate_code() {
555566
$code = random_string(10);
556567
}
557568
}
558-
569+
559570
return $code;
560571
}
572+
573+
// Digits with hyphens
574+
private static function generate_code_digits_with_hyphens() {
575+
global $DB;
576+
577+
// Define the character set (digits only).
578+
$characters = '0123456789';
579+
$charCount = strlen($characters); // Cache the length to optimize loop performance
580+
$length = 12; // Total length excluding hyphens
581+
582+
do {
583+
// Generate a raw code
584+
$rawcode = '';
585+
for ($i = 0; $i < $length; $i++) {
586+
$rawcode .= $characters[random_int(0, $charCount - 1)]; // Secure random number selection
587+
}
588+
589+
// Format the code as XXXX-XXXX-XXXX
590+
$code = substr($rawcode, 0, 4) . '-' . substr($rawcode, 4, 4) . '-' . substr($rawcode, 8, 4);
591+
592+
// Check if the generated code already exists in the database
593+
$exists = $DB->record_exists('customcert_issues', ['code' => $code]);
594+
595+
} while ($exists); // Repeat until a unique code is found
596+
597+
return $code;
598+
}
561599
}

db/install.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
</KEYS>
5858
<INDEXES>
5959
<INDEX NAME="userid-customcertid" UNIQUE="false" FIELDS="userid, customcertid"/>
60-
<INDEX NAME="code" UNIQUE="false" FIELDS="code"/>
60+
<INDEX NAME="code" UNIQUE="true" FIELDS="code"/>
6161
</INDEXES>
6262
</TABLE>
6363
<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

lang/en/customcert.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,7 @@
243243
$string['verifycertificatedesc'] = 'This link will take you to a new screen where you will be able to verify certificates on the site';
244244
$string['width'] = 'Width';
245245
$string['width_help'] = 'This is the width of the certificate PDF in mm. For reference an A4 piece of paper is 210mm wide and a letter is 216mm wide.';
246+
$string['codegenerationmethod'] = 'Code generation method';
247+
$string['codegenerationmethod_desc'] = 'Choose between the two methods for generating certificate codes.';
248+
$string['Upper/lower/digits'] = '6aOdbLEuoC (Upper/lower/digits random string)';
249+
$string['digits-with-hyphens'] = '0123-4567-8901 (Digits with hyphens)';

lib.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,63 @@ function mod_customcert_inplace_editable($itemtype, $itemid, $newvalue) {
424424
}
425425
}
426426

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

settings.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@
8181
$settings->add(new admin_setting_heading('defaults',
8282
get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin')));
8383

84+
$settings->add(new admin_setting_configselect(
85+
'customcert/codegenerationmethod',
86+
get_string('codegenerationmethod', 'customcert'),
87+
get_string('codegenerationmethod_desc', 'customcert'),
88+
0, // Default option (0 = Upper/lower/digits random string method)
89+
[
90+
0 => get_string('Upper/lower/digits', 'customcert'), // Upper/lower/digits random string
91+
1 => get_string('digits-with-hyphens', 'customcert') // Digits with hyphens numeric code
92+
]
93+
));
94+
8495
$yesnooptions = [
8596
0 => get_string('no'),
8697
1 => get_string('yes'),

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)