Skip to content
Open
68 changes: 47 additions & 21 deletions classes/certificate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}

/**
Expand All @@ -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;
}

/**
Expand All @@ -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;
}
}
19 changes: 0 additions & 19 deletions db/upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
101 changes: 88 additions & 13 deletions lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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;
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
*/
Expand Down
Loading
Loading