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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of using a hard-coded variable like 0. It doesn't mean much to the developer using it.

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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you tell me why we are editing this function?

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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need an upgrade clause to remove these settings from the Moodle DB if it exists, probably use something like unset_config() iirc. Please add another upgrade step to do this and just leave this as is.

$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 (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks worse than before. Was this change necessary?

($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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be used anywhere.

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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lib.php file is for functions that Moodle will call itself. We don't need to put it there. I would suggest creating a new class in the classes folder for this functionality.

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