Skip to content

Commit f191bde

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 45e7acd commit f191bde

File tree

8 files changed

+209
-47
lines changed

8 files changed

+209
-47
lines changed

classes/certificate.php

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -552,49 +552,62 @@ public static function issue_certificate($certificateid, $userid) {
552552
return $issueid;
553553
}
554554

555-
/**
556-
* Generates an unused code of random letters and numbers.
557-
*
558-
* @return string
559-
*/
560-
public static function generate_code(): string {
555+
public static function generate_code() {
561556
global $DB;
562557

563-
// Get the user's selected method from settings.
558+
// Get the user's selected method from settings
564559
$method = get_config('customcert', 'codegenerationmethod');
565560

566-
do {
567-
$code = match ($method) {
568-
'0' => self::generate_code_upper_lower_digits(),
569-
'1' => self::generate_code_digits_with_hyphens(),
570-
default => self::generate_code_upper_lower_digits(),
571-
};
572-
} while ($DB->record_exists('customcert_issues', ['code' => $code]));
573-
return $code;
561+
// If the upper/lower/digits is selected (0), use the upper/lower/digits code generation method
562+
if ($method == 0) {
563+
return self::generate_code_upper_lower_digits();
564+
}
565+
566+
// Otherwise, use the digits with hyphens method (1)
567+
return self::generate_code_digits_with_hyphens();
574568
}
575569

576-
/**
577-
* Generate a random code of the format XXXXXXXXXX, where each X is a character from the set [A-Za-z0-9].
578-
* Does not check that it is unused.
579-
*
580-
* @return string
581-
*/
582-
private static function generate_code_upper_lower_digits(): string {
583-
return random_string(10);
570+
// Upper/lower/digits random string
571+
private static function generate_code_upper_lower_digits() {
572+
global $DB;
573+
574+
$uniquecodefound = false;
575+
$code = random_string(10);
576+
while (!$uniquecodefound) {
577+
if (!$DB->record_exists('customcert_issues', ['code' => $code])) {
578+
$uniquecodefound = true;
579+
} else {
580+
$code = random_string(10);
581+
}
582+
}
583+
584+
return $code;
584585
}
585586

586-
/**
587-
* Generate an random code of the format XXXX-XXXX-XXXX, where each X is a random digit.
588-
* Does not check that it is unused.
589-
*
590-
* @return string
591-
*/
592-
private static function generate_code_digits_with_hyphens(): string {
593-
return sprintf(
594-
'%04d-%04d-%04d',
595-
random_int(0, 9999),
596-
random_int(0, 9999),
597-
random_int(0, 9999)
598-
);
587+
// Digits with hyphens
588+
private static function generate_code_digits_with_hyphens() {
589+
global $DB;
590+
591+
// Define the character set (digits only).
592+
$characters = '0123456789';
593+
$charCount = strlen($characters); // Cache the length to optimize loop performance
594+
$length = 12; // Total length excluding hyphens
595+
596+
do {
597+
// Generate a raw code
598+
$rawcode = '';
599+
for ($i = 0; $i < $length; $i++) {
600+
$rawcode .= $characters[random_int(0, $charCount - 1)]; // Secure random number selection
601+
}
602+
603+
// Format the code as XXXX-XXXX-XXXX
604+
$code = substr($rawcode, 0, 4) . '-' . substr($rawcode, 4, 4) . '-' . substr($rawcode, 8, 4);
605+
606+
// Check if the generated code already exists in the database
607+
$exists = $DB->record_exists('customcert_issues', ['code' => $code]);
608+
609+
} while ($exists); // Repeat until a unique code is found
610+
611+
return $code;
599612
}
600613
}

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,7 @@
249249
$string['verifycertificatedesc'] = 'This link will take you to a new screen where you will be able to verify certificates on the site';
250250
$string['width'] = 'Width';
251251
$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.';
252-
252+
$string['codegenerationmethod'] = 'Code generation method';
253+
$string['codegenerationmethod_desc'] = 'Choose between the two methods for generating certificate codes.';
254+
$string['Upper/lower/digits'] = '6aOdbLEuoC (Upper/lower/digits random string)';
255+
$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
@@ -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
*/

settings.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@
8585
'customcert/codegenerationmethod',
8686
get_string('codegenerationmethod', 'customcert'),
8787
get_string('codegenerationmethod_desc', 'customcert'),
88-
0, // Default option (0 = Upper/lower/digits random string method).
88+
0, // Default option (0 = Upper/lower/digits random string method)
8989
[
90-
0 => get_string('codegenerationmethod_upperlowerdigits', 'customcert'), // Upper/lower/digits random string.
91-
1 => get_string('codegenerationmethod_digitshyphens', 'customcert'), // Digits with hyphens numeric code.
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
9292
]
9393
));
9494

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)