diff --git a/classes/certificate.php b/classes/certificate.php index 10e4a9dd..90f3147e 100644 --- a/classes/certificate.php +++ b/classes/certificate.php @@ -34,7 +34,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class certificate { - /** * Send the file inline to the browser. */ @@ -293,7 +292,7 @@ public static function download_all_issues_for_instance(\mod_customcert\template public static function download_all_for_site(): void { global $DB; - list($namefields, $nameparams) = \core_user\fields::get_sql_fullname(); + [$namefields, $nameparams] = \core_user\fields::get_sql_fullname(); $sql = "SELECT ci.*, $namefields as fullname, ct.id as templateid, ct.name as templatename, ct.contextid FROM {customcert_issues} ci JOIN {user} u @@ -350,7 +349,7 @@ public static function get_issues($customcertid, $groupmode, $cm, $limitfrom, $l global $DB; // Get the conditional SQL. - list($conditionssql, $conditionsparams) = self::get_conditional_issues_sql($cm, $groupmode); + [$conditionssql, $conditionsparams] = self::get_conditional_issues_sql($cm, $groupmode); // If it is empty then return an empty array. if (empty($conditionsparams)) { @@ -389,7 +388,7 @@ public static function get_number_of_issues($customcertid, $cm, $groupmode) { global $DB; // Get the conditional SQL. - list($conditionssql, $conditionsparams) = self::get_conditional_issues_sql($cm, $groupmode); + [$conditionssql, $conditionsparams] = self::get_conditional_issues_sql($cm, $groupmode); // If it is empty then return 0. if (empty($conditionsparams)) { @@ -428,7 +427,7 @@ public static function get_conditional_issues_sql($cm, $groupmode) { // Get all users that can manage this certificate to exclude them from the report. $certmanagers = array_keys(get_users_by_capability($context, 'mod/customcert:manage', 'u.id')); $certmanagers = array_merge($certmanagers, array_keys(get_admins())); - list($sql, $params) = $DB->get_in_or_equal($certmanagers, SQL_PARAMS_NAMED, 'cert'); + [$sql, $params] = $DB->get_in_or_equal($certmanagers, SQL_PARAMS_NAMED, 'cert'); $conditionssql .= "AND NOT u.id $sql \n"; $conditionsparams += $params; @@ -464,7 +463,7 @@ public static function get_conditional_issues_sql($cm, $groupmode) { return ['', []]; } - list($sql, $params) = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED, 'grp'); + [$sql, $params] = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED, 'grp'); $conditionssql .= "AND u.id $sql "; $conditionsparams += $params; } @@ -563,14 +562,13 @@ public static function generate_code(): string { // Get the user's selected method from settings. $method = get_config('customcert', 'codegenerationmethod'); - do { - $code = match ($method) { - '0' => self::generate_code_upper_lower_digits(), - '1' => self::generate_code_digits_with_hyphens(), - default => self::generate_code_upper_lower_digits(), - }; - } while ($DB->record_exists('customcert_issues', ['code' => $code])); - return $code; + // If the upper/lower/digits is selected (0), use the upper/lower/digits code generation method. + if ($method == 0) { + return self::generate_code_upper_lower_digits(); + } + + // Otherwise, use the digits with hyphens method (1). + return self::generate_code_digits_with_hyphens(); } /** @@ -580,7 +578,19 @@ public static function generate_code(): string { * @return string */ private static function generate_code_upper_lower_digits(): string { - return random_string(10); + global $DB; + + $uniquecodefound = false; + $code = random_string(10); + while (!$uniquecodefound) { + if (!$DB->record_exists('customcert_issues', ['code' => $code])) { + $uniquecodefound = true; + } else { + $code = random_string(10); + } + } + + return $code; } /** @@ -590,11 +600,27 @@ private static function generate_code_upper_lower_digits(): string { * @return string */ private static function generate_code_digits_with_hyphens(): string { - return sprintf( - '%04d-%04d-%04d', - random_int(0, 9999), - random_int(0, 9999), - random_int(0, 9999) - ); + global $DB; + + // Define the character set (digits only). + $characters = '0123456789'; + $charcount = strlen($characters); // Cache the length to optimize loop performance. + $length = 12; // Total length excluding hyphens. + + do { + // Generate a raw code. + $rawcode = ''; + for ($i = 0; $i < $length; $i++) { + $rawcode .= $characters[random_int(0, $charcount - 1)]; // Secure random number selection. + } + + // Format the code as XXXX-XXXX-XXXX. + $code = substr($rawcode, 0, 4) . '-' . substr($rawcode, 4, 4) . '-' . substr($rawcode, 8, 4); + + // Check if the generated code already exists in the database. + $exists = $DB->record_exists('customcert_issues', ['code' => $code]); + } while ($exists); // Repeat until a unique code is found. + + return $code; } } diff --git a/db/upgrade.php b/db/upgrade.php index ad60bb29..f0cf6a77 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -316,24 +316,5 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2024042210, 'mod', 'customcert'); } - if ($oldversion < 2024042213) { - $table = new xmldb_table('customcert'); - - // Add 'usecustomfilename' field. - $field = new xmldb_field('usecustomfilename', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'deliveryoption'); - if (!$dbman->field_exists($table, $field)) { - $dbman->add_field($table, $field); - } - - // Add 'customfilenamepattern' field. - $field = new xmldb_field('customfilenamepattern', XMLDB_TYPE_TEXT, null, null, null, null, null, 'usecustomfilename'); - if (!$dbman->field_exists($table, $field)) { - $dbman->add_field($table, $field); - } - - // Savepoint reached. - upgrade_mod_savepoint(true, 2024042213, 'customcert'); - } - return true; } diff --git a/lib.php b/lib.php index 69afda51..eb476d49 100644 --- a/lib.php +++ b/lib.php @@ -350,18 +350,26 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na if (has_capability('mod/customcert:manage', $settings->get_page()->cm->context)) { // Get the template id. $templateid = $DB->get_field('customcert', 'templateid', ['id' => $settings->get_page()->cm->instance]); - $node = navigation_node::create(get_string('editcustomcert', 'customcert'), - new moodle_url('/mod/customcert/edit.php', ['tid' => $templateid]), - navigation_node::TYPE_SETTING, null, 'mod_customcert_edit', - new pix_icon('t/edit', '')); + $node = navigation_node::create( + get_string('editcustomcert', 'customcert'), + new moodle_url('/mod/customcert/edit.php', ['tid' => $templateid]), + navigation_node::TYPE_SETTING, + null, + 'mod_customcert_edit', + new pix_icon('t/edit', '') + ); $customcertnode->add_node($node, $beforekey); } if (has_capability('mod/customcert:verifycertificate', $settings->get_page()->cm->context)) { - $node = navigation_node::create(get_string('verifycertificate', 'customcert'), + $node = navigation_node::create( + get_string('verifycertificate', 'customcert'), new moodle_url('/mod/customcert/verify_certificate.php', ['contextid' => $settings->get_page()->cm->context->id]), - navigation_node::TYPE_SETTING, null, 'mod_customcert_verify_certificate', - new pix_icon('t/check', '')); + navigation_node::TYPE_SETTING, + null, + 'mod_customcert_verify_certificate', + new pix_icon('t/check', '') + ); $customcertnode->add_node($node, $beforekey); } @@ -380,8 +388,10 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na function mod_customcert_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { global $USER; - if (($user->id != $USER->id) - && !has_capability('mod/customcert:viewallcertificates', context_system::instance())) { + if ( + ($user->id != $USER->id) + && !has_capability('mod/customcert:viewallcertificates', context_system::instance()) + ) { return; } @@ -392,8 +402,13 @@ function mod_customcert_myprofile_navigation(core_user\output\myprofile\tree $tr $params['course'] = $course->id; } $url = new moodle_url('/mod/customcert/my_certificates.php', $params); - $node = new core_user\output\myprofile\node('miscellaneous', 'mycustomcerts', - get_string('mycertificates', 'customcert'), null, $url); + $node = new core_user\output\myprofile\node( + 'miscellaneous', + 'mycustomcerts', + get_string('mycertificates', 'customcert'), + null, + $url + ); $tree->add_node($node); } @@ -431,11 +446,71 @@ function mod_customcert_inplace_editable($itemtype, $itemid, $newvalue) { $updateelement->name = clean_param($newvalue, PARAM_TEXT); $DB->update_record('customcert_elements', $updateelement); - return new \core\output\inplace_editable('mod_customcert', 'elementname', $element->id, true, - $updateelement->name, $updateelement->name); + return new \core\output\inplace_editable( + 'mod_customcert', + 'elementname', + $element->id, + true, + $updateelement->name, + $updateelement->name + ); } } +/** + * Generates a public URL for viewing a user's certificate (eCard). + * + * This function constructs a URL that allows public access to a certificate + * without requiring authentication. It does so by generating a secure token + * based on the certificate code. + * + * @param string $certcode The unique code of the certificate. + * @return string The generated public URL for the certificate. + */ +function generate_public_url_for_certificate(string $certcode): string { + global $CFG; + + // Generate a security token for the certificate using a private function. + $token = calculate_signature($certcode); + + // Construct and return the public URL to view the certificate. + return $CFG->wwwroot . '/mod/customcert/view_user_cert.php?cert_code=' . urlencode($certcode) . '&token=' . urlencode($token); +} + +/** + * Generates a secure HMAC signature for a certificate. + * + * This function creates a unique signature for a certificate based on its code. + * The signature is used as a security token to verify access to the certificate. + * It prevents unauthorized access by ensuring that only valid certificates can + * be accessed through a generated URL. + * + * The signature is generated using the HMAC (Hash-based Message Authentication Code) + * method with SHA-256, ensuring strong security. It uses Moodle's `siteidentifier` + * as the secret key, making it unique to each Moodle installation. + * + * @param string $certcode The unique certificate code. + * @return string The generated HMAC signature. + */ +function calculate_signature(string $certcode): string { + global $CFG; + + // Define a namespaced message prefix to avoid signature collisions. + $messageprefix = 'mod_customcert:view_user_cert'; + + // Construct the message that will be signed. + // This includes the prefix and the certificate code to create a unique hash. + $message = $messageprefix . '|' . $certcode; + + // Use Moodle's unique site identifier as the secret key for HMAC. + // This ensures that signatures are installation-specific. + $secret = $CFG->siteidentifier; + + // Generate the HMAC hash using SHA-256. + // This provides a cryptographic signature that is difficult to forge. + return hash_hmac('sha256', $message, $secret); +} + /** * Get icon mapping for font-awesome. */ diff --git a/view_user_cert.php b/view_user_cert.php new file mode 100644 index 00000000..c18e6df4 --- /dev/null +++ b/view_user_cert.php @@ -0,0 +1,110 @@ +. +// Include required Moodle configuration and custom certificate library. + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->dirroot . '/mod/customcert/lib.php'); + +// Allows guest access to course with ID 1. +require_login(1, false); + +// Set up the page context before processing any parameters. +// This ensures that Moodle properly initializes the page and handles any errors gracefully. +$context = context_system::instance(); +$PAGE->set_context($context); +$PAGE->set_url('/mod/customcert/view_user_cert.php'); +$PAGE->set_title('View certificate'); +$PAGE->set_heading('View certificate'); + +/** + * Displays an error message in a formatted Moodle page and exits. + * + * This function helps standardize error handling by rendering the page + * properly and showing the error message in an alert box. + * + * Code fragment to define the version of the customcert module + * + * @package mod_customcert + * @copyright 2013 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @param string $message The error message to display. + */ +function display_error_page($message) { + global $OUTPUT; + + echo $OUTPUT->header(); // Display the page header. + echo $OUTPUT->box($message, 'alert alert-danger'); // Display the error message in a styled box. + echo $OUTPUT->footer(); // Display the page footer. + exit; // Stop further execution. +} + +// Retrieve certificate code and verification token from URL parameters. +// 'optional_param' is used instead of 'required_param' to avoid Moodle throwing an automatic error page. +$certcode = optional_param('cert_code', '', PARAM_ALPHANUMEXT); +$token = optional_param('token', '', PARAM_ALPHANUMEXT); + +// Ensure both required parameters are provided. +if (empty($certcode) || empty($token)) { + display_error_page('Certificate code or verification token is missing. Please check the URL and try again.'); +} + +// Validate the provided token by regenerating it using the expected algorithm. +$expectedtoken = calculate_signature($certcode); +if ($token !== $expectedtoken) { + display_error_page('The verification token is invalid for this certificate. Please check the URL and try again.'); +} + +// Retrieve the certificate issue entry +// using the provided certificate code. +// This helps fetch the associated user ID to verify ownership. +$issue = $DB->get_record('customcert_issues', ['code' => $certcode], '*'); + +if (!$issue) { + display_error_page('Certificate with this code not found. ' + . 'Please check the code and try again.'); +} + +// Fetch the certificate associated with the retrieved issue. +$certificate = $DB->get_record('customcert', ['id' => $issue->customcertid]); +if (!$certificate) { + display_error_page('The certificate does not exist. Please contact the site administrator for assistance.'); +} + +// Retrieve the corresponding template for the fetched certificate. +// The template defines the layout and content of the generated certificate. +$template = $DB->get_record('customcert_templates', ['id' => $certificate->templateid]); +if (!$template) { + display_error_page('The certificate template could not be found. Please contact the site administrator for assistance.'); +} + +try { + // Convert the template record into a template object. + // This object provides methods to generate and render the certificate. + $template = new \mod_customcert\template($template); + + // Generate and output the certificate PDF. + // 'false' indicates that the PDF is displayed inline, + // instead of being force-downloaded. + // The second parameter ensures the certificate is generated for the correct user. + $template->generate_pdf(false, $issue->userid); +} catch (Exception $e) { + // Catch any errors that may occur while generating the certificate PDF. + display_error_page('Error generating certificate PDF. ' + . 'Try again later or contact support.'); +} + +// Prevent further execution after rendering the certificate. +exit;