From 91cf2d1922d88f98ee3afaa54cc45cf17c8c5ce7 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:24:58 +0200 Subject: [PATCH 01/36] add keeplocalcopy field to the customcert table --- db/install.xml | 1 + db/upgrade.php | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/db/install.xml b/db/install.xml index c54a4a56..a80a95b9 100644 --- a/db/install.xml +++ b/db/install.xml @@ -22,6 +22,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 4c96702f..94ce2f9c 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -200,5 +200,17 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_mod_savepoint(true, 2022041903, 'customcert'); // Replace with the actual version number. } + if ($oldversion < 2023101000) { + $table = new xmldb_table('customcert'); + $field = new xmldb_field('keeplocalcopy', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'language'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2023101000, 'customcert'); // Replace with the actual version number. + } + return true; } From 95e98623891a03ee64b4f088298f9d430d310500 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:26:20 +0200 Subject: [PATCH 02/36] add managekeeplocalcopy, deletelocalcopy capabilities --- db/access.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/db/access.php b/db/access.php index 0d6b89c6..3a098288 100644 --- a/db/access.php +++ b/db/access.php @@ -105,6 +105,26 @@ ) ), + 'mod/customcert:managekeeplocalcopy' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ), + + 'mod/customcert:deletelocalcopy' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ), + 'mod/customcert:manageemailstudents' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, From ec00528d183705a3b974163047eccc72c34834db Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:27:26 +0200 Subject: [PATCH 03/36] add keeplocalcopy to the backup structure --- backup/moodle2/backup_customcert_stepslib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backup/moodle2/backup_customcert_stepslib.php b/backup/moodle2/backup_customcert_stepslib.php index e1f80fbe..88aa438b 100644 --- a/backup/moodle2/backup_customcert_stepslib.php +++ b/backup/moodle2/backup_customcert_stepslib.php @@ -41,7 +41,7 @@ protected function define_structure() { // The instance. $customcert = new backup_nested_element('customcert', array('id'), array( 'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', 'emailstudents', - 'emailteachers', 'emailothers', 'protection', 'timecreated', 'timemodified')); + 'emailteachers', 'emailothers', 'protection', 'timecreated', 'timemodified', 'keeplocalcopy')); // The template. $template = new backup_nested_element('template', array('id'), array( From 65892a55e950996828c9df93b9f42cd5c5e70be5 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:31:59 +0200 Subject: [PATCH 04/36] add keeplocalcopy setting element to forms --- mod_form.php | 8 ++++++++ settings.php | 2 ++ 2 files changed, 10 insertions(+) diff --git a/mod_form.php b/mod_form.php index 00bb5b5c..dc4ad77f 100644 --- a/mod_form.php +++ b/mod_form.php @@ -66,6 +66,13 @@ public function definition() { $mform->addElement('select', 'deliveryoption', get_string('deliveryoptions', 'customcert'), $deliveryoptions); $mform->setDefault('deliveryoption', certificate::DELIVERY_OPTION_INLINE); + if (has_capability('mod/customcert:managekeeplocalcopy', $this->get_context())) { + $mform->addElement('selectyesno', 'keeplocalcopy', get_string('keeplocalcopy', 'customcert')); + $mform->setDefault('keeplocalcopy', get_config('customcert', 'keeplocalcopy')); + $mform->addHelpButton('keeplocalcopy', 'keeplocalcopy', 'customcert'); + $mform->setType('keeplocalcopy', PARAM_INT); + } + if (has_capability('mod/customcert:manageemailstudents', $this->get_context())) { $mform->addElement('selectyesno', 'emailstudents', get_string('emailstudents', 'customcert')); $mform->setDefault('emailstudents', get_config('customcert', 'emailstudents')); @@ -207,6 +214,7 @@ public function validation($data, $files) { */ protected function get_options_elements_with_required_caps() { return [ + 'keeplocalcopy' => 'mod/customcert:managekeeplocalcopy', 'emailstudents' => 'mod/customcert:manageemailstudents', 'emailteachers' => 'mod/customcert:manageemailteachers', 'emailothers' => 'mod/customcert:manageemailothers', diff --git a/settings.php b/settings.php index 6a648edc..d0399606 100644 --- a/settings.php +++ b/settings.php @@ -59,6 +59,8 @@ 1 => get_string('yes'), ]; +$settings->add(new admin_setting_configselect('customcert/keeplocalcopy', + get_string('keeplocalcopy', 'customcert'), get_string('keeplocalcopy_help', 'customcert'), 0, $yesnooptions)); $settings->add(new admin_setting_configselect('customcert/emailstudents', get_string('emailstudents', 'customcert'), get_string('emailstudents_help', 'customcert'), 0, $yesnooptions)); $settings->add(new admin_setting_configselect('customcert/emailteachers', From 22ca94e362bcefa2dbccc521c77fb34afa425a2d Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:33:33 +0200 Subject: [PATCH 05/36] add localfile class to manage local PDF files --- classes/localfile.php | 237 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 classes/localfile.php diff --git a/classes/localfile.php b/classes/localfile.php new file mode 100644 index 00000000..112a651f --- /dev/null +++ b/classes/localfile.php @@ -0,0 +1,237 @@ +. + +/** + * Class represents a local file of an issued certificate. + * + * @package mod_customcert + * @copyright 2023 Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_customcert; + +use file_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class represents a local file of an issued certificate. + * + * @package mod_customcert + * @copyright 023 Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class localfile { + + /** + * The template representing the content of the file. + * + * @var \mod_customcert\template + */ + protected $template; + + /** + * The component name for the file storage. + */ + const component = 'mod_customcert'; + + /** + * The filearea name for the file storage. + */ + const filearea = 'customcert_issues'; + + /** + * The constructor. + * + * @param \mod_customcert\template $template + */ + public function __construct(\mod_customcert\template $template) { + $this->template = $template; + } + + /** + * Save the PDF to the file storage. + * + * @param string $pdfcontent string content of the pdf + * @param integer|null $userid the id of the user whose certificate we want to save + * @return stored_file|false the stored_file object on success, false on error + */ + public function savePDF(string $pdfcontent, ?int $userid = null) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + if (empty($userid)) { + $userid = $USER->id; + } + + try { + $file = $this->getPDF($userid); + if (!$file) { + // Create file containing the pdf + $fs = get_file_storage(); + $file = $fs->create_file_from_string($this->buildFileInfo($userid), $pdfcontent); + } + return $file; + } catch (file_exception $e) { + // maybe log the exception + return false; + } + } + + /** + * Get the PDF from the file storage. + * + * @param integer|null $userid the id of the user whose certificate we want to get + * @return \stored_file|false the stored_file object on success, false on error + */ + public function getPDF(?int $userid = null) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + if (empty($userid)) { + $userid = $USER->id; + } + + $fileinfo = $this->buildFileInfo($userid); + $fs = get_file_storage(); + return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], + $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + } + + /** + * Delete the PDF from the file storage. + * + * @param integer|null $userid the id of the user whose certificate we want to get + * @return bool true on success + */ + public function deletePDF(?int $userid = null) { + global $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + try { + $file = $this->getPDF($userid); + if ($file) { + return $file->delete(); + } + return false; + } catch (file_exception $e) { + // maybe log the exception + return false; + } + } + + /** + * Send the PDF to the browser or return it as a string. + * + * @param int $userid the id of the user whose certificate we want to view + * @param int $deliveryoption the delivery option of the customcert + * @param bool $return Do we want to return the contents of the PDF? + * @return string|void Can return the PDF in string format if specified. + */ + public function sendPDF(?int $userid = NULL, int $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { + global $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + $file = $this->getPDF($userid); + if ($file) { + if ($return) { + return $file->get_content(); + } else { + // send the file to the browser + send_stored_file( + $file, + 0, + 0, + $deliveryoption == certificate::DELIVERY_OPTION_DOWNLOAD, + ['filename' => $file->get_filename()] + ); + die(); + } + } + } + + /** + * Check if a pdf exists in the file storage area. + * + * @param \stdClass $cm the course module + * @param integer|null $userid the id of the user whose PDF we want to check + * @param integer|null $templateid the template id of the customcert we want to check + * @return \stored_file|false the stored_file object on success, false on error + */ + public static function existsPDF($cm, ?int $userid = null, ?int $templateid = null) { + + $fileinfo = self::buildFileInfoArr($cm, $userid, $templateid); + $fs = get_file_storage(); + return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], + $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + } + + /** + * Build the fileinfo array needed by the file storage. + * + * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @return array the fileinfo array + */ + protected function buildFileInfo(?int $userid = null) { + + return self::buildFileInfoArr($this->template->get_cm(), $userid, $this->template->get_id()); + } + + /** + * Build the fileinfo array needed by the file storage, static version. + * + * @param \stdClass $cm the course module + * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @param integer|null $templateid the template id of the customcert of the array we want to generate + * @return array the fileinfo array + */ + private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templateid = null) { + + /** @var \moodle_database $DB */ + global $DB, $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + if (empty($templateid)) { + $customcert = $DB->get_record('customcert', array('id' => $cm->instance), '*', MUST_EXIST); + $templateid = $customcert->templateid; + } + + $course = $DB->get_record('course', ['id' => $cm->course]); + $context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $course->id]); + $user_info = $DB->get_record('user', ['id' => $userid]); + + return [ + 'contextid' => $context->id, + 'component' => self::component, + 'filearea' => self::filearea, + 'itemid' => $templateid, + 'userid' => $USER->id, + 'author' => fullname($USER), + 'filepath' => '/' . $course->id . '/', + 'filename' => $user_info->username . '_cert-' . $templateid . '_course-' . $course->shortname . '.pdf' + ]; + } +} From 8ecc09a76416b1f970b173401e098951cbebbb0b Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:34:33 +0200 Subject: [PATCH 06/36] add logic to store and serve local PDF files --- classes/template.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/classes/template.php b/classes/template.php index 03da0309..8fb6b50d 100644 --- a/classes/template.php +++ b/classes/template.php @@ -48,6 +48,11 @@ class template { */ protected $contextid; + /** + * @var \mod_customcert\localfile the local file for the template. + */ + protected $localfile; + /** * The constructor. * @@ -57,6 +62,7 @@ public function __construct($template) { $this->id = $template->id; $this->name = $template->name; $this->contextid = $template->contextid; + $this->localfile = new localfile($this); } /** @@ -326,6 +332,13 @@ public function generate_pdf(bool $preview = false, int $userid = null, bool $re $deliveryoption = $customcert->deliveryoption; } + if ($customcert->keeplocalcopy) { + $retval = $this->localfile->sendPDF($userid, $deliveryoption, $return); + if ($return && !empty($retval)) { + return $retval; + } + } + // Remove full-stop at the end, if it exists, to avoid "..pdf" being created and being filtered by clean_filename. $filename = rtrim(format_string($this->name, true, ['context' => $this->get_context()]), '.'); @@ -374,6 +387,10 @@ public function generate_pdf(bool $preview = false, int $userid = null, bool $re } } + if ($customcert->keeplocalcopy) { + $this->localfile->savePDF($pdf->Output('', 'S'), $userid); + } + if ($return) { return $pdf->Output('', 'S'); } From b3c1b992fb60edc5745a7181b63c1c1bb503d328 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:35:19 +0200 Subject: [PATCH 07/36] add deletelocalcopy to the actions column --- classes/report_table.php | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/classes/report_table.php b/classes/report_table.php index 40cc4ab2..df652fad 100644 --- a/classes/report_table.php +++ b/classes/report_table.php @@ -177,16 +177,42 @@ public function col_download($user) { public function col_actions($user) { global $OUTPUT; - $icon = new \pix_icon('i/delete', get_string('delete')); - $link = new \moodle_url('/mod/customcert/view.php', + $actions = [ [ - 'id' => $this->cm->id, - 'deleteissue' => $user->issueid, - 'sesskey' => sesskey() + 'icon' => new \pix_icon('i/delete', get_string('delete')), + 'link' => new \moodle_url( + '/mod/customcert/view.php', + [ + 'id' => $this->cm->id, + 'deleteissue' => $user->issueid, + 'sesskey' => sesskey() + ] + ), + 'attributes' => ['class' => 'action-icon delete-icon'], ] - ); + ]; + + if (has_capability('mod/customcert:deletelocalcopy', \context_module::instance($this->cm->id)) && localfile::existsPDF($this->cm, $user->id)) { + $actions[] = [ + 'icon' => new \pix_icon('deletelocalcopy', get_string('deletelocalcopy', 'customcert'), 'customcert'), + 'link' => new \moodle_url( + '/mod/customcert/view.php', + [ + 'id' => $this->cm->id, + 'deleteissue' => $user->issueid, + 'deletelocalcopy' => 1, + 'sesskey' => sesskey() + ] + ), + 'attributes' => ['class' => 'action-icon deletelocalcopy-icon'], + ]; + } - return $OUTPUT->action_icon($link, $icon, null, ['class' => 'action-icon delete-icon']); + return implode(" ", array_map( + fn ($action) => + $OUTPUT->action_icon($action['link'], $action['icon'], null, $action['attributes'] ?? []), + $actions + )); } /** From c35d84f09b9d6962f4accbd54922e9d63afd112a Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:35:40 +0200 Subject: [PATCH 08/36] add deletelocalcopy logic --- view.php | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/view.php b/view.php index aba682ea..55932649 100644 --- a/view.php +++ b/view.php @@ -29,6 +29,7 @@ $downloadtable = optional_param('download', null, PARAM_ALPHA); $downloadissue = optional_param('downloadissue', 0, PARAM_INT); $deleteissue = optional_param('deleteissue', 0, PARAM_INT); +$deletelocalcopy = optional_param('deletelocalcopy', 0, PARAM_INT); $confirm = optional_param('confirm', false, PARAM_BOOL); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', \mod_customcert\certificate::CUSTOMCERT_PER_PAGE, PARAM_INT); @@ -46,6 +47,7 @@ $canreceive = has_capability('mod/customcert:receiveissue', $context); $canmanage = has_capability('mod/customcert:manage', $context); $canviewreport = has_capability('mod/customcert:viewreport', $context); +$candeletelocalcopy = has_capability('mod/customcert:deletelocalcopy', $context); // Initialise $PAGE. $pageurl = new moodle_url('/mod/customcert/view.php', array('id' => $cm->id)); @@ -70,14 +72,20 @@ [ 'id' => $id, 'deleteissue' => $deleteissue, + 'deletelocalcopy' => $deletelocalcopy, 'confirm' => 1, 'sesskey' => sesskey() ] ); // Show a confirmation page. - $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); - $message = get_string('deleteissueconfirm', 'customcert'); + if ($deletelocalcopy) { + $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); + $message = get_string('deletelocalcopyconfirm', 'customcert'); + } else if ($deleteissue) { + $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); + $message = get_string('deleteissueconfirm', 'customcert'); + } echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($customcert->name)); echo $OUTPUT->confirm($message, $yesurl, $nourl); @@ -85,8 +93,19 @@ exit(); } - // Delete the issue. - $DB->delete_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + // Always delete local copy. + if ($candeletelocalcopy) { + $issues = $DB->get_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + if (!empty($issues)) { + $lf = new \mod_customcert\localfile(new \mod_customcert\template($template)); + array_map(fn($issue) => $lf->deletePDF($issue->userid), $issues); + } + } + + if (!$deletelocalcopy) { + // Delete the issue. + $DB->delete_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + } // Redirect back to the manage templates page. redirect(new moodle_url('/mod/customcert/view.php', array('id' => $id))); From d4828c02e5ad4498588be4b7a941dfdce226e0a6 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:36:58 +0200 Subject: [PATCH 09/36] add 'download all certificates' link to nav menus --- lib.php | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/lib.php b/lib.php index 8014da3a..29ef1ccc 100644 --- a/lib.php +++ b/lib.php @@ -312,6 +312,27 @@ function mod_customcert_output_fragment_editelement($args) { return $form->render(); } +/** + * This function extends the course navigation block for the site. + * + * @param \navigation_node $parentnode + * @param \stdClass $course + * @param \context_course $context + */ +function customcert_extend_navigation_course(\navigation_node $parentnode, \stdClass $course, \context_course $context) { + global $PAGE; + + $addnode = $context->contextlevel === 50; + $addnode = $addnode && !($context->instanceid === SITEID); + $addnode = $addnode && has_capability('mod/customcert:viewallcertificates', $context); + $isCourseNav = is_null($PAGE->cm->instance); + if ($addnode && $isCourseNav) { + if ($node = build_downloadall_node($isCourseNav, $course, $context)) { + $parentnode->add_node($node); + } + } +} + /** * This function extends the settings navigation block for the site. * @@ -343,6 +364,12 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na $customcertnode->add_node($node, $beforekey); } + if (has_capability('mod/customcert:viewallcertificates', $PAGE->cm->context)) { + if ($node = build_downloadall_node(false, $PAGE->cm->get_course(), null)) { + $customcertnode->add_node($node, $beforekey); + } + } + if (has_capability('mod/customcert:verifycertificate', $settings->get_page()->cm->context)) { $node = navigation_node::create(get_string('verifycertificate', 'customcert'), new moodle_url('/mod/customcert/verify_certificate.php', array('contextid' => $settings->get_page()->cm->context->id)), @@ -354,6 +381,50 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na return $customcertnode->trim_if_empty(); } +/** + * Build the navigation node for the 'Download certificates' item. + * + * @param boolean $isCourseNav + * @param \stdClass $course + * @param \course_context $context + * @return \navigation_node|null null if there are no certifcates available for the download + */ +function build_downloadall_node(bool $isCourseNav, stdClass $course, \context_course $context = null) { + + global $DB, $PAGE; + + if (!$isCourseNav) { + $courseContext = \context_course::instance($course->id); + //Check if there is available certs + $certs = $DB->get_records('customcert', ['id' => $PAGE->cm->instance]); + $users = $DB->get_records('role_assignments', ['contextid' => $courseContext->id]); + $urlparams = ['customcertid' => $PAGE->cm->instance]; + } else { + $certs = $DB->get_records('customcert', ['course' => $context->instanceid]); + $users = $DB->get_records('role_assignments', ['contextid' => $context->id]); + $urlparams = ['courseid' => $course->id]; + } + $availablecerts = false; + foreach ($certs as $certid => $cert_fields) { + foreach ($users as $userid => $user_fields) { + if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { + continue; + } + $availablecerts = true; + break; + } + if ($availablecerts) break; + } + if ($availablecerts) { + $node = \navigation_node::create(get_string('bulkdownloadlink', 'mod_customcert'), + new \moodle_url('/mod/customcert/downloadcerts.php', $urlparams), + \navigation_node::TYPE_SETTING, null, 'mod_customcert_downloadcerts', + new \pix_icon('a/download_all', 'certificates')); + return $node; + } + return null; +} + /** * Add nodes to myprofile page. * @@ -420,7 +491,8 @@ function mod_customcert_inplace_editable($itemtype, $itemid, $newvalue) { */ function mod_customcert_get_fontawesome_icon_map() { return [ - 'mod_customcert:download' => 'fa-download' + 'mod_customcert:deletelocalcopy' => 'fa-minus-square', + 'mod_customcert:download' => 'fa-download', ]; } From 90f00065b8ffdbf321933edea61c4e9bae668042 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:37:20 +0200 Subject: [PATCH 10/36] add file to download all certificates --- downloadcerts.php | 106 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 downloadcerts.php diff --git a/downloadcerts.php b/downloadcerts.php new file mode 100644 index 00000000..f2d28d85 --- /dev/null +++ b/downloadcerts.php @@ -0,0 +1,106 @@ +. + +/** + * Handles zip and download of certificates. + * + * Derived from the local_bulkcustomcert by Gonzalo Romero. + * + * @package mod_customcert + * @author Gonzalo Romero + * @author Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../config.php'); + +$courseid = optional_param('courseid', null, PARAM_INT); +$customcertid = optional_param('customcertid', null, PARAM_INT); + +if (!has_capability('mod/customcert:viewallcertificates', context_system::instance()) && !$courseid && !$customcert) { + die(); +} + +global $DB; + +// Increase the server timeout to handle the creation and sending of large zip files. +core_php_time_limit::raise(); + +if ($courseid) { + $course = $DB->get_record('course', ['id' => $courseid]); + $certs = $DB->get_records('customcert', ['course' => $courseid]); +} else if ($customcertid) { + $cert = $DB->get_record('customcert', ['id' => $customcertid], '*', MUST_EXIST); + $certs[$cert->id] = $cert; + $course = $DB->get_record('course', ['id' => $certs[$cert->id]->course], '*', MUST_EXIST); + $courseid = $course->id; + unset($cert); +} + +$context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $courseid]); +$users = $DB->get_records('role_assignments', ['contextid' => $context->id]); + +// Build a list of files to zip. +$filesforzipping = []; + +foreach ($certs as $certid => $cert_fields) { + $template = null; + foreach ($users as $userid => $user_fields) { + if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { + continue; + } + if (is_null($template)) { + $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); + $template = new \mod_customcert\template($template); + } + $lf = new \mod_customcert\localfile($template); + if (false === $file = $lf->getPDF($user_fields->userid)) { + // must generate the pdf + $pdf = $template->generate_pdf(false, $user_fields->userid, true); + if (!empty($pdf)) { + $file = $lf->getPDF($user_fields->userid); + } + } + if ($file) { + $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' .$file->get_filename()] = $file; + } + } +} + +if (count($filesforzipping) == 0) { + // This should never happen. The option only show up if there is available certs. + $url = new moodle_url('/course/view.php?id=' . $courseid); + redirect($url); +} else if ($zipfile = pack_files($filesforzipping)) { + send_temp_file($zipfile, get_string('modulenameplural', 'customcert') . '-' . $course->shortname . '.zip'); +} +die(); + + +function pack_files($filesforzipping) +{ + global $CFG; + // Create path for new zip file. + $tempzip = tempnam($CFG->tempdir . '/', 'customcert_'); + // Zip files. + $zipper = new zip_packer(); + if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) { + return $tempzip; + } + return false; +} From 381ee0f5912f5c6afe1ba5d448dd30b77303a6ea Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:37:37 +0200 Subject: [PATCH 11/36] add lang strings --- lang/en/customcert.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 28910b8f..3783da2b 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -41,6 +41,8 @@ $string['createtemplate'] = 'Create template'; $string['customcert:addinstance'] = 'Add a new custom certificate instance'; $string['customcert:manage'] = 'Manage a custom certificate'; +$string['customcert:managekeeplocalcopy'] = 'Manage keep local certificate copy setting'; +$string['customcert:deletelocalcopy'] = 'Delete local certificate copies'; $string['customcert:manageemailstudents'] = 'Manage email students setting'; $string['customcert:manageemailteachers'] = 'Manage email teachers setting'; $string['customcert:manageemailothers'] = 'Manage email others setting'; @@ -59,6 +61,7 @@ $string['deleteelement'] = 'Delete element'; $string['deleteelementconfirm'] = 'Are you sure you want to delete this element?'; $string['deleteissueconfirm'] = 'Are you sure you want to delete this certificate issue?'; +$string['deletelocalcopyconfirm'] = 'Are you sure you want to delete this certificate PDF file?'; $string['deleteissuedcertificates'] = 'Delete issued certificates'; $string['deletepageconfirm'] = 'Are you sure you want to delete this certificate page?'; $string['deletetemplateconfirm'] = 'Are you sure you want to delete this certificate template?'; @@ -95,6 +98,10 @@ $string['emailstudentsubject'] = '{$a->coursefullname}: {$a->certificatename}'; $string['emailstudents'] = 'Email students'; $string['emailstudents_help'] = 'If set this will email the students a copy of the certificate when it becomes available. Warning: Setting this to \'Yes\' before you have finished creating the certificate will email the student an incomplete certificate.'; +$string['keeplocalcopy'] = 'Keep local certificate copy'; +$string['keeplocalcopy_help'] = 'If set this will keep a local copy of the certificate PDF. The copy will be served on all future certificate downloads until the file is deleted from the moodle files table.'; +$string['deletelocalcopy'] = 'Delete local certificate copy'; +$string['bulkdownloadlink'] = 'Download certificates'; $string['emailteachers'] = 'Email teachers'; $string['emailteachers_help'] = 'If set this will email the teachers a copy of the certificate when it becomes available. Warning: Setting this to \'Yes\' before you have finished creating the certificate will email the teacher an incomplete certificate.'; $string['emailothers'] = 'Email others'; From 054c01b4e66d1630adea1450e54201015aab3aca Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:38:18 +0200 Subject: [PATCH 12/36] Bump version --- version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.php b/version.php index b771b1ac..d48efd3e 100644 --- a/version.php +++ b/version.php @@ -24,10 +24,10 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2023042402; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2023101000; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2023042400; // Requires this Moodle version (4.2). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; $plugin->maturity = MATURITY_STABLE; -$plugin->release = "4.2.2"; // User-friendly version number. +$plugin->release = "4.2.3"; // User-friendly version number. From 394834007453ca23c3af27187d66b57dd5ed91b3 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:39:08 +0200 Subject: [PATCH 13/36] italian translation for new lang strings --- lang/it/customcert.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lang/it/customcert.php diff --git a/lang/it/customcert.php b/lang/it/customcert.php new file mode 100644 index 00000000..0a8753e5 --- /dev/null +++ b/lang/it/customcert.php @@ -0,0 +1,31 @@ +. + +/** + * Language strings for the customcert module. + * + * @package mod_customcert + * @copyright 2013 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + $string['keeplocalcopy'] = 'Mantieni copia dei certificati'; + $string['keeplocalcopy_help'] = 'Se impostato a Sì, mantiene una copia del PDF dei certificati. La copia sarà scaricata ad ogni futura richiesta del certificato, finché il file non è cancellato dalla tabella files di moodle.'; + $string['deletelocalcopy'] = 'Cancella copia locale del certificato'; + $string['bulkdownloadlink'] = 'Download certificati'; + $string['deletelocalcopyconfirm'] = 'Sei sicuro di voler cancellare il file PDF del certificato?'; + $string['customcert:managekeeplocalcopy'] = 'Gestire impostazioni copia locale certificati'; + $string['customcert:deletelocalcopy'] = 'Cancella copia locale dei certificati'; From 8d31f9d937ae8f7e4b24b74cdd804e60cc6e1a64 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Mon, 16 Oct 2023 15:46:52 +0200 Subject: [PATCH 14/36] fix sendPDF method signature --- classes/localfile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/localfile.php b/classes/localfile.php index 112a651f..22cff578 100644 --- a/classes/localfile.php +++ b/classes/localfile.php @@ -141,11 +141,11 @@ public function deletePDF(?int $userid = null) { * Send the PDF to the browser or return it as a string. * * @param int $userid the id of the user whose certificate we want to view - * @param int $deliveryoption the delivery option of the customcert + * @param string $deliveryoption the delivery option of the customcert * @param bool $return Do we want to return the contents of the PDF? * @return string|void Can return the PDF in string format if specified. */ - public function sendPDF(?int $userid = NULL, int $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { + public function sendPDF(?int $userid = NULL, string $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { global $USER; if (empty($userid)) { From c25341b7446a1e80d6662ee438b9fc9aac52dc3a Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Tue, 17 Oct 2023 11:43:34 +0200 Subject: [PATCH 15/36] add buildFileName static method --- classes/localfile.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/classes/localfile.php b/classes/localfile.php index 22cff578..e4102bd1 100644 --- a/classes/localfile.php +++ b/classes/localfile.php @@ -231,7 +231,19 @@ private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templa 'userid' => $USER->id, 'author' => fullname($USER), 'filepath' => '/' . $course->id . '/', - 'filename' => $user_info->username . '_cert-' . $templateid . '_course-' . $course->shortname . '.pdf' + 'filename' => self::buildFileName($user_info->username, $templateid, $course->shortname), ]; } + + /** + * Build the PDF filename. + * + * @param string $username + * @param string $templateid + * @param string $courseShortname + * @return string the PDF file name + */ + public static function buildFileName($username, $templateid, $courseShortname) { + return $username . '_cert-' . $templateid . '_course-' . $courseShortname . '.pdf'; + } } From 86e95c0ff8930ff7dc0a98b7d9e00799e16d16f8 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Tue, 17 Oct 2023 11:44:32 +0200 Subject: [PATCH 16/36] fix to download certs when keeplocalcopy is false --- downloadcerts.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/downloadcerts.php b/downloadcerts.php index f2d28d85..9d9dc981 100644 --- a/downloadcerts.php +++ b/downloadcerts.php @@ -36,6 +36,7 @@ die(); } +/** @var \moodle_database $DB */ global $DB; // Increase the server timeout to handle the creation and sending of large zip files. @@ -54,13 +55,15 @@ $context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $courseid]); $users = $DB->get_records('role_assignments', ['contextid' => $context->id]); +list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($u) => $u->userid, $users), SQL_PARAMS_NAMED); +$usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); // Build a list of files to zip. $filesforzipping = []; foreach ($certs as $certid => $cert_fields) { $template = null; - foreach ($users as $userid => $user_fields) { + foreach ($users as $roleAssignmentId => $user_fields) { if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { continue; } @@ -73,11 +76,18 @@ // must generate the pdf $pdf = $template->generate_pdf(false, $user_fields->userid, true); if (!empty($pdf)) { - $file = $lf->getPDF($user_fields->userid); + if ($cert_fields->keeplocalcopy) { + $file = $lf->getPDF($user_fields->userid); + } else { + $file = [ + 'content' => $pdf, + ]; + } } } if ($file) { - $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' .$file->get_filename()] = $file; + $filename = \mod_customcert\localfile::buildFileName($usersObjs[$user_fields->userid]->username, $template->get_id(), $course->shortname); + $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' . $filename] = $file; } } } From b2ee500adaf51a85ba72782de0ecad15668470de Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Mon, 20 Nov 2023 12:53:12 +0100 Subject: [PATCH 17/36] bug fix and obey keeplocalcopy setting --- downloadcerts.php | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/downloadcerts.php b/downloadcerts.php index 9d9dc981..d6779317 100644 --- a/downloadcerts.php +++ b/downloadcerts.php @@ -21,7 +21,7 @@ * * @package mod_customcert * @author Gonzalo Romero - * @author Giorgio Consorti + * @author Giorgio Consorti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -53,31 +53,23 @@ unset($cert); } -$context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $courseid]); -$users = $DB->get_records('role_assignments', ['contextid' => $context->id]); -list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($u) => $u->userid, $users), SQL_PARAMS_NAMED); -$usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); - // Build a list of files to zip. $filesforzipping = []; foreach ($certs as $certid => $cert_fields) { - $template = null; - foreach ($users as $roleAssignmentId => $user_fields) { - if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { - continue; - } - if (is_null($template)) { - $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); - $template = new \mod_customcert\template($template); - } - $lf = new \mod_customcert\localfile($template); - if (false === $file = $lf->getPDF($user_fields->userid)) { + $issues = $DB->get_records('customcert_issues', ['customcertid' => $certid]); + list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($i) => $i->userid, $issues), SQL_PARAMS_NAMED); + $usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); + $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); + $template = new \mod_customcert\template($template); + $lf = new \mod_customcert\localfile($template); + foreach ($issues as $issue) { + if (false === $file = $lf->getPDF($issue->userid)) { // must generate the pdf - $pdf = $template->generate_pdf(false, $user_fields->userid, true); + $pdf = $template->generate_pdf(false, $issue->userid, true); if (!empty($pdf)) { if ($cert_fields->keeplocalcopy) { - $file = $lf->getPDF($user_fields->userid); + $file = $lf->getPDF($issue->userid); } else { $file = [ 'content' => $pdf, @@ -86,7 +78,7 @@ } } if ($file) { - $filename = \mod_customcert\localfile::buildFileName($usersObjs[$user_fields->userid]->username, $template->get_id(), $course->shortname); + $filename = \mod_customcert\localfile::buildFileName($usersObjs[$issue->userid]->username, $template->get_id(), $course->shortname); $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' . $filename] = $file; } } From 57ce9a6f37f91df27d319cb7efeb642d50d510c5 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:24:58 +0200 Subject: [PATCH 18/36] add keeplocalcopy field to the customcert table --- db/install.xml | 1 + db/upgrade.php | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/db/install.xml b/db/install.xml index c4226720..1eff50a6 100644 --- a/db/install.xml +++ b/db/install.xml @@ -22,6 +22,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index c089d027..fe34659f 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -235,5 +235,17 @@ function xmldb_customcert_upgrade($oldversion) { upgrade_mod_savepoint(true, 2023042405, 'customcert'); } + if ($oldversion < 2023101000) { + $table = new xmldb_table('customcert'); + $field = new xmldb_field('keeplocalcopy', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'language'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2023101000, 'customcert'); // Replace with the actual version number. + } + return true; } From 6ba8d3ff915ba291739d71db478054aa71683ce3 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:26:20 +0200 Subject: [PATCH 19/36] add managekeeplocalcopy, deletelocalcopy capabilities --- db/access.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/db/access.php b/db/access.php index f3f71555..78770706 100644 --- a/db/access.php +++ b/db/access.php @@ -104,6 +104,26 @@ ] ], + 'mod/customcert:managekeeplocalcopy' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ], + + 'mod/customcert:deletelocalcopy' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ], + 'mod/customcert:manageemailstudents' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, From 48b5e90862c09933a07ed92e401f9caf3693280c Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:27:26 +0200 Subject: [PATCH 20/36] add keeplocalcopy to the backup structure --- backup/moodle2/backup_customcert_stepslib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backup/moodle2/backup_customcert_stepslib.php b/backup/moodle2/backup_customcert_stepslib.php index 01dc0f75..52d6616f 100644 --- a/backup/moodle2/backup_customcert_stepslib.php +++ b/backup/moodle2/backup_customcert_stepslib.php @@ -41,7 +41,7 @@ protected function define_structure() { // The instance. $customcert = new backup_nested_element('customcert', ['id'], [ 'templateid', 'name', 'intro', 'introformat', 'requiredtime', 'verifyany', 'emailstudents', - 'emailteachers', 'emailothers', 'protection', 'timecreated', 'timemodified']); + 'emailteachers', 'emailothers', 'protection', 'timecreated', 'timemodified', 'keeplocalcopy']); // The template. $template = new backup_nested_element('template', ['id'], [ From 79d2f4d9c2327a3f0f53188f61f1a2af9db634d8 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:31:59 +0200 Subject: [PATCH 21/36] add keeplocalcopy setting element to forms --- mod_form.php | 8 ++++++++ settings.php | 2 ++ 2 files changed, 10 insertions(+) diff --git a/mod_form.php b/mod_form.php index 917deec2..1e947bd2 100644 --- a/mod_form.php +++ b/mod_form.php @@ -66,6 +66,13 @@ public function definition() { $mform->addElement('select', 'deliveryoption', get_string('deliveryoptions', 'customcert'), $deliveryoptions); $mform->setDefault('deliveryoption', certificate::DELIVERY_OPTION_INLINE); + if (has_capability('mod/customcert:managekeeplocalcopy', $this->get_context())) { + $mform->addElement('selectyesno', 'keeplocalcopy', get_string('keeplocalcopy', 'customcert')); + $mform->setDefault('keeplocalcopy', get_config('customcert', 'keeplocalcopy')); + $mform->addHelpButton('keeplocalcopy', 'keeplocalcopy', 'customcert'); + $mform->setType('keeplocalcopy', PARAM_INT); + } + if (has_capability('mod/customcert:manageemailstudents', $this->get_context())) { $mform->addElement('selectyesno', 'emailstudents', get_string('emailstudents', 'customcert')); $mform->setDefault('emailstudents', get_config('customcert', 'emailstudents')); @@ -207,6 +214,7 @@ public function validation($data, $files) { */ protected function get_options_elements_with_required_caps() { return [ + 'keeplocalcopy' => 'mod/customcert:managekeeplocalcopy', 'emailstudents' => 'mod/customcert:manageemailstudents', 'emailteachers' => 'mod/customcert:manageemailteachers', 'emailothers' => 'mod/customcert:manageemailothers', diff --git a/settings.php b/settings.php index 6a648edc..d0399606 100644 --- a/settings.php +++ b/settings.php @@ -59,6 +59,8 @@ 1 => get_string('yes'), ]; +$settings->add(new admin_setting_configselect('customcert/keeplocalcopy', + get_string('keeplocalcopy', 'customcert'), get_string('keeplocalcopy_help', 'customcert'), 0, $yesnooptions)); $settings->add(new admin_setting_configselect('customcert/emailstudents', get_string('emailstudents', 'customcert'), get_string('emailstudents_help', 'customcert'), 0, $yesnooptions)); $settings->add(new admin_setting_configselect('customcert/emailteachers', From ab1bf52436837968e58f2f934b5d6d7b1ad99cef Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:33:33 +0200 Subject: [PATCH 22/36] add localfile class to manage local PDF files --- classes/localfile.php | 237 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 classes/localfile.php diff --git a/classes/localfile.php b/classes/localfile.php new file mode 100644 index 00000000..112a651f --- /dev/null +++ b/classes/localfile.php @@ -0,0 +1,237 @@ +. + +/** + * Class represents a local file of an issued certificate. + * + * @package mod_customcert + * @copyright 2023 Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_customcert; + +use file_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class represents a local file of an issued certificate. + * + * @package mod_customcert + * @copyright 023 Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class localfile { + + /** + * The template representing the content of the file. + * + * @var \mod_customcert\template + */ + protected $template; + + /** + * The component name for the file storage. + */ + const component = 'mod_customcert'; + + /** + * The filearea name for the file storage. + */ + const filearea = 'customcert_issues'; + + /** + * The constructor. + * + * @param \mod_customcert\template $template + */ + public function __construct(\mod_customcert\template $template) { + $this->template = $template; + } + + /** + * Save the PDF to the file storage. + * + * @param string $pdfcontent string content of the pdf + * @param integer|null $userid the id of the user whose certificate we want to save + * @return stored_file|false the stored_file object on success, false on error + */ + public function savePDF(string $pdfcontent, ?int $userid = null) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + if (empty($userid)) { + $userid = $USER->id; + } + + try { + $file = $this->getPDF($userid); + if (!$file) { + // Create file containing the pdf + $fs = get_file_storage(); + $file = $fs->create_file_from_string($this->buildFileInfo($userid), $pdfcontent); + } + return $file; + } catch (file_exception $e) { + // maybe log the exception + return false; + } + } + + /** + * Get the PDF from the file storage. + * + * @param integer|null $userid the id of the user whose certificate we want to get + * @return \stored_file|false the stored_file object on success, false on error + */ + public function getPDF(?int $userid = null) { + global $CFG, $USER; + require_once($CFG->libdir . '/filelib.php'); + + if (empty($userid)) { + $userid = $USER->id; + } + + $fileinfo = $this->buildFileInfo($userid); + $fs = get_file_storage(); + return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], + $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + } + + /** + * Delete the PDF from the file storage. + * + * @param integer|null $userid the id of the user whose certificate we want to get + * @return bool true on success + */ + public function deletePDF(?int $userid = null) { + global $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + try { + $file = $this->getPDF($userid); + if ($file) { + return $file->delete(); + } + return false; + } catch (file_exception $e) { + // maybe log the exception + return false; + } + } + + /** + * Send the PDF to the browser or return it as a string. + * + * @param int $userid the id of the user whose certificate we want to view + * @param int $deliveryoption the delivery option of the customcert + * @param bool $return Do we want to return the contents of the PDF? + * @return string|void Can return the PDF in string format if specified. + */ + public function sendPDF(?int $userid = NULL, int $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { + global $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + $file = $this->getPDF($userid); + if ($file) { + if ($return) { + return $file->get_content(); + } else { + // send the file to the browser + send_stored_file( + $file, + 0, + 0, + $deliveryoption == certificate::DELIVERY_OPTION_DOWNLOAD, + ['filename' => $file->get_filename()] + ); + die(); + } + } + } + + /** + * Check if a pdf exists in the file storage area. + * + * @param \stdClass $cm the course module + * @param integer|null $userid the id of the user whose PDF we want to check + * @param integer|null $templateid the template id of the customcert we want to check + * @return \stored_file|false the stored_file object on success, false on error + */ + public static function existsPDF($cm, ?int $userid = null, ?int $templateid = null) { + + $fileinfo = self::buildFileInfoArr($cm, $userid, $templateid); + $fs = get_file_storage(); + return $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], + $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']); + } + + /** + * Build the fileinfo array needed by the file storage. + * + * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @return array the fileinfo array + */ + protected function buildFileInfo(?int $userid = null) { + + return self::buildFileInfoArr($this->template->get_cm(), $userid, $this->template->get_id()); + } + + /** + * Build the fileinfo array needed by the file storage, static version. + * + * @param \stdClass $cm the course module + * @param integer|null $userid the id of the user whose fileinfo array we want to generate + * @param integer|null $templateid the template id of the customcert of the array we want to generate + * @return array the fileinfo array + */ + private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templateid = null) { + + /** @var \moodle_database $DB */ + global $DB, $USER; + + if (empty($userid)) { + $userid = $USER->id; + } + + if (empty($templateid)) { + $customcert = $DB->get_record('customcert', array('id' => $cm->instance), '*', MUST_EXIST); + $templateid = $customcert->templateid; + } + + $course = $DB->get_record('course', ['id' => $cm->course]); + $context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $course->id]); + $user_info = $DB->get_record('user', ['id' => $userid]); + + return [ + 'contextid' => $context->id, + 'component' => self::component, + 'filearea' => self::filearea, + 'itemid' => $templateid, + 'userid' => $USER->id, + 'author' => fullname($USER), + 'filepath' => '/' . $course->id . '/', + 'filename' => $user_info->username . '_cert-' . $templateid . '_course-' . $course->shortname . '.pdf' + ]; + } +} From 663fc6c097751865446075b8fb2a8464f039f6b0 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:34:33 +0200 Subject: [PATCH 23/36] add logic to store and serve local PDF files --- classes/template.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/classes/template.php b/classes/template.php index 444af034..5792f886 100644 --- a/classes/template.php +++ b/classes/template.php @@ -48,6 +48,11 @@ class template { */ protected $contextid; + /** + * @var \mod_customcert\localfile the local file for the template. + */ + protected $localfile; + /** * The constructor. * @@ -57,6 +62,7 @@ public function __construct($template) { $this->id = $template->id; $this->name = $template->name; $this->contextid = $template->contextid; + $this->localfile = new localfile($this); } /** @@ -314,6 +320,13 @@ public function generate_pdf(bool $preview = false, int $userid = null, bool $re $deliveryoption = $customcert->deliveryoption; } + if ($customcert->keeplocalcopy) { + $retval = $this->localfile->sendPDF($userid, $deliveryoption, $return); + if ($return && !empty($retval)) { + return $retval; + } + } + // Remove full-stop at the end, if it exists, to avoid "..pdf" being created and being filtered by clean_filename. $filename = rtrim(format_string($this->name, true, ['context' => $this->get_context()]), '.'); @@ -362,6 +375,10 @@ public function generate_pdf(bool $preview = false, int $userid = null, bool $re } } + if ($customcert->keeplocalcopy) { + $this->localfile->savePDF($pdf->Output('', 'S'), $userid); + } + if ($return) { return $pdf->Output('', 'S'); } From b8ae136813330d4c000a9ab9308bd98313645955 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:35:19 +0200 Subject: [PATCH 24/36] add deletelocalcopy to the actions column --- classes/report_table.php | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/classes/report_table.php b/classes/report_table.php index 40cc4ab2..df652fad 100644 --- a/classes/report_table.php +++ b/classes/report_table.php @@ -177,16 +177,42 @@ public function col_download($user) { public function col_actions($user) { global $OUTPUT; - $icon = new \pix_icon('i/delete', get_string('delete')); - $link = new \moodle_url('/mod/customcert/view.php', + $actions = [ [ - 'id' => $this->cm->id, - 'deleteissue' => $user->issueid, - 'sesskey' => sesskey() + 'icon' => new \pix_icon('i/delete', get_string('delete')), + 'link' => new \moodle_url( + '/mod/customcert/view.php', + [ + 'id' => $this->cm->id, + 'deleteissue' => $user->issueid, + 'sesskey' => sesskey() + ] + ), + 'attributes' => ['class' => 'action-icon delete-icon'], ] - ); + ]; + + if (has_capability('mod/customcert:deletelocalcopy', \context_module::instance($this->cm->id)) && localfile::existsPDF($this->cm, $user->id)) { + $actions[] = [ + 'icon' => new \pix_icon('deletelocalcopy', get_string('deletelocalcopy', 'customcert'), 'customcert'), + 'link' => new \moodle_url( + '/mod/customcert/view.php', + [ + 'id' => $this->cm->id, + 'deleteissue' => $user->issueid, + 'deletelocalcopy' => 1, + 'sesskey' => sesskey() + ] + ), + 'attributes' => ['class' => 'action-icon deletelocalcopy-icon'], + ]; + } - return $OUTPUT->action_icon($link, $icon, null, ['class' => 'action-icon delete-icon']); + return implode(" ", array_map( + fn ($action) => + $OUTPUT->action_icon($action['link'], $action['icon'], null, $action['attributes'] ?? []), + $actions + )); } /** From 37cd6a431e7a9eabe71da9576148ab5964e37cad Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:35:40 +0200 Subject: [PATCH 25/36] add deletelocalcopy logic --- view.php | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/view.php b/view.php index 04c2ae2e..f66d401f 100644 --- a/view.php +++ b/view.php @@ -29,6 +29,7 @@ $downloadtable = optional_param('download', null, PARAM_ALPHA); $downloadissue = optional_param('downloadissue', 0, PARAM_INT); $deleteissue = optional_param('deleteissue', 0, PARAM_INT); +$deletelocalcopy = optional_param('deletelocalcopy', 0, PARAM_INT); $confirm = optional_param('confirm', false, PARAM_BOOL); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', \mod_customcert\certificate::CUSTOMCERT_PER_PAGE, PARAM_INT); @@ -46,6 +47,7 @@ $canreceive = has_capability('mod/customcert:receiveissue', $context); $canmanage = has_capability('mod/customcert:manage', $context); $canviewreport = has_capability('mod/customcert:viewreport', $context); +$candeletelocalcopy = has_capability('mod/customcert:deletelocalcopy', $context); // Initialise $PAGE. $pageurl = new moodle_url('/mod/customcert/view.php', ['id' => $cm->id]); @@ -70,14 +72,20 @@ [ 'id' => $id, 'deleteissue' => $deleteissue, + 'deletelocalcopy' => $deletelocalcopy, 'confirm' => 1, 'sesskey' => sesskey() ] ); // Show a confirmation page. - $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); - $message = get_string('deleteissueconfirm', 'customcert'); + if ($deletelocalcopy) { + $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); + $message = get_string('deletelocalcopyconfirm', 'customcert'); + } else if ($deleteissue) { + $PAGE->navbar->add(get_string('deleteconfirm', 'customcert')); + $message = get_string('deleteissueconfirm', 'customcert'); + } echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($customcert->name)); echo $OUTPUT->confirm($message, $yesurl, $nourl); @@ -87,6 +95,19 @@ // Delete the issue. $DB->delete_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); + // Always delete local copy. + if ($candeletelocalcopy) { + $issues = $DB->get_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + if (!empty($issues)) { + $lf = new \mod_customcert\localfile(new \mod_customcert\template($template)); + array_map(fn($issue) => $lf->deletePDF($issue->userid), $issues); + } + } + + if (!$deletelocalcopy) { + // Delete the issue. + $DB->delete_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + } // Redirect back to the manage templates page. redirect(new moodle_url('/mod/customcert/view.php', ['id' => $id])); From 11038d376bbea73a32432915f057e2311e60575f Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:36:58 +0200 Subject: [PATCH 26/36] add 'download all certificates' link to nav menus --- lib.php | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/lib.php b/lib.php index 819aec3f..a97b7818 100644 --- a/lib.php +++ b/lib.php @@ -312,6 +312,27 @@ function mod_customcert_output_fragment_editelement($args) { return $form->render(); } +/** + * This function extends the course navigation block for the site. + * + * @param \navigation_node $parentnode + * @param \stdClass $course + * @param \context_course $context + */ +function customcert_extend_navigation_course(\navigation_node $parentnode, \stdClass $course, \context_course $context) { + global $PAGE; + + $addnode = $context->contextlevel === 50; + $addnode = $addnode && !($context->instanceid === SITEID); + $addnode = $addnode && has_capability('mod/customcert:viewallcertificates', $context); + $isCourseNav = is_null($PAGE->cm->instance); + if ($addnode && $isCourseNav) { + if ($node = build_downloadall_node($isCourseNav, $course, $context)) { + $parentnode->add_node($node); + } + } +} + /** * This function extends the settings navigation block for the site. * @@ -343,6 +364,12 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na $customcertnode->add_node($node, $beforekey); } + if (has_capability('mod/customcert:viewallcertificates', $PAGE->cm->context)) { + if ($node = build_downloadall_node(false, $PAGE->cm->get_course(), null)) { + $customcertnode->add_node($node, $beforekey); + } + } + if (has_capability('mod/customcert:verifycertificate', $settings->get_page()->cm->context)) { $node = navigation_node::create(get_string('verifycertificate', 'customcert'), new moodle_url('/mod/customcert/verify_certificate.php', ['contextid' => $settings->get_page()->cm->context->id]), @@ -354,6 +381,50 @@ function customcert_extend_settings_navigation(settings_navigation $settings, na return $customcertnode->trim_if_empty(); } +/** + * Build the navigation node for the 'Download certificates' item. + * + * @param boolean $isCourseNav + * @param \stdClass $course + * @param \course_context $context + * @return \navigation_node|null null if there are no certifcates available for the download + */ +function build_downloadall_node(bool $isCourseNav, stdClass $course, \context_course $context = null) { + + global $DB, $PAGE; + + if (!$isCourseNav) { + $courseContext = \context_course::instance($course->id); + //Check if there is available certs + $certs = $DB->get_records('customcert', ['id' => $PAGE->cm->instance]); + $users = $DB->get_records('role_assignments', ['contextid' => $courseContext->id]); + $urlparams = ['customcertid' => $PAGE->cm->instance]; + } else { + $certs = $DB->get_records('customcert', ['course' => $context->instanceid]); + $users = $DB->get_records('role_assignments', ['contextid' => $context->id]); + $urlparams = ['courseid' => $course->id]; + } + $availablecerts = false; + foreach ($certs as $certid => $cert_fields) { + foreach ($users as $userid => $user_fields) { + if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { + continue; + } + $availablecerts = true; + break; + } + if ($availablecerts) break; + } + if ($availablecerts) { + $node = \navigation_node::create(get_string('bulkdownloadlink', 'mod_customcert'), + new \moodle_url('/mod/customcert/downloadcerts.php', $urlparams), + \navigation_node::TYPE_SETTING, null, 'mod_customcert_downloadcerts', + new \pix_icon('a/download_all', 'certificates')); + return $node; + } + return null; +} + /** * Add nodes to myprofile page. * @@ -427,7 +498,8 @@ function mod_customcert_inplace_editable($itemtype, $itemid, $newvalue) { */ function mod_customcert_get_fontawesome_icon_map() { return [ - 'mod_customcert:download' => 'fa-download' + 'mod_customcert:deletelocalcopy' => 'fa-minus-square', + 'mod_customcert:download' => 'fa-download', ]; } From f67cfa9695f7ef2c5d696fb6df974e75668390ad Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:37:20 +0200 Subject: [PATCH 27/36] add file to download all certificates --- downloadcerts.php | 106 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 downloadcerts.php diff --git a/downloadcerts.php b/downloadcerts.php new file mode 100644 index 00000000..f2d28d85 --- /dev/null +++ b/downloadcerts.php @@ -0,0 +1,106 @@ +. + +/** + * Handles zip and download of certificates. + * + * Derived from the local_bulkcustomcert by Gonzalo Romero. + * + * @package mod_customcert + * @author Gonzalo Romero + * @author Giorgio Consorti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../config.php'); + +$courseid = optional_param('courseid', null, PARAM_INT); +$customcertid = optional_param('customcertid', null, PARAM_INT); + +if (!has_capability('mod/customcert:viewallcertificates', context_system::instance()) && !$courseid && !$customcert) { + die(); +} + +global $DB; + +// Increase the server timeout to handle the creation and sending of large zip files. +core_php_time_limit::raise(); + +if ($courseid) { + $course = $DB->get_record('course', ['id' => $courseid]); + $certs = $DB->get_records('customcert', ['course' => $courseid]); +} else if ($customcertid) { + $cert = $DB->get_record('customcert', ['id' => $customcertid], '*', MUST_EXIST); + $certs[$cert->id] = $cert; + $course = $DB->get_record('course', ['id' => $certs[$cert->id]->course], '*', MUST_EXIST); + $courseid = $course->id; + unset($cert); +} + +$context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $courseid]); +$users = $DB->get_records('role_assignments', ['contextid' => $context->id]); + +// Build a list of files to zip. +$filesforzipping = []; + +foreach ($certs as $certid => $cert_fields) { + $template = null; + foreach ($users as $userid => $user_fields) { + if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { + continue; + } + if (is_null($template)) { + $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); + $template = new \mod_customcert\template($template); + } + $lf = new \mod_customcert\localfile($template); + if (false === $file = $lf->getPDF($user_fields->userid)) { + // must generate the pdf + $pdf = $template->generate_pdf(false, $user_fields->userid, true); + if (!empty($pdf)) { + $file = $lf->getPDF($user_fields->userid); + } + } + if ($file) { + $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' .$file->get_filename()] = $file; + } + } +} + +if (count($filesforzipping) == 0) { + // This should never happen. The option only show up if there is available certs. + $url = new moodle_url('/course/view.php?id=' . $courseid); + redirect($url); +} else if ($zipfile = pack_files($filesforzipping)) { + send_temp_file($zipfile, get_string('modulenameplural', 'customcert') . '-' . $course->shortname . '.zip'); +} +die(); + + +function pack_files($filesforzipping) +{ + global $CFG; + // Create path for new zip file. + $tempzip = tempnam($CFG->tempdir . '/', 'customcert_'); + // Zip files. + $zipper = new zip_packer(); + if ($zipper->archive_to_pathname($filesforzipping, $tempzip)) { + return $tempzip; + } + return false; +} From 3420bac7594338f572c591cd246c35964e1a37db Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:37:37 +0200 Subject: [PATCH 28/36] add lang strings --- lang/en/customcert.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lang/en/customcert.php b/lang/en/customcert.php index 28910b8f..3783da2b 100644 --- a/lang/en/customcert.php +++ b/lang/en/customcert.php @@ -41,6 +41,8 @@ $string['createtemplate'] = 'Create template'; $string['customcert:addinstance'] = 'Add a new custom certificate instance'; $string['customcert:manage'] = 'Manage a custom certificate'; +$string['customcert:managekeeplocalcopy'] = 'Manage keep local certificate copy setting'; +$string['customcert:deletelocalcopy'] = 'Delete local certificate copies'; $string['customcert:manageemailstudents'] = 'Manage email students setting'; $string['customcert:manageemailteachers'] = 'Manage email teachers setting'; $string['customcert:manageemailothers'] = 'Manage email others setting'; @@ -59,6 +61,7 @@ $string['deleteelement'] = 'Delete element'; $string['deleteelementconfirm'] = 'Are you sure you want to delete this element?'; $string['deleteissueconfirm'] = 'Are you sure you want to delete this certificate issue?'; +$string['deletelocalcopyconfirm'] = 'Are you sure you want to delete this certificate PDF file?'; $string['deleteissuedcertificates'] = 'Delete issued certificates'; $string['deletepageconfirm'] = 'Are you sure you want to delete this certificate page?'; $string['deletetemplateconfirm'] = 'Are you sure you want to delete this certificate template?'; @@ -95,6 +98,10 @@ $string['emailstudentsubject'] = '{$a->coursefullname}: {$a->certificatename}'; $string['emailstudents'] = 'Email students'; $string['emailstudents_help'] = 'If set this will email the students a copy of the certificate when it becomes available. Warning: Setting this to \'Yes\' before you have finished creating the certificate will email the student an incomplete certificate.'; +$string['keeplocalcopy'] = 'Keep local certificate copy'; +$string['keeplocalcopy_help'] = 'If set this will keep a local copy of the certificate PDF. The copy will be served on all future certificate downloads until the file is deleted from the moodle files table.'; +$string['deletelocalcopy'] = 'Delete local certificate copy'; +$string['bulkdownloadlink'] = 'Download certificates'; $string['emailteachers'] = 'Email teachers'; $string['emailteachers_help'] = 'If set this will email the teachers a copy of the certificate when it becomes available. Warning: Setting this to \'Yes\' before you have finished creating the certificate will email the teacher an incomplete certificate.'; $string['emailothers'] = 'Email others'; From f148bae8c212eb17f16cc32abb1f0461dde95fee Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:38:18 +0200 Subject: [PATCH 29/36] Bump version --- version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.php b/version.php index f6a0bdfb..d48efd3e 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.'); -$plugin->version = 2023042406; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2023101000; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2023042400; // Requires this Moodle version (4.2). $plugin->cron = 0; // Period for cron to check this module (secs). $plugin->component = 'mod_customcert'; From b10606b8ded634285286efeb642f9724f559c708 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Fri, 13 Oct 2023 12:39:08 +0200 Subject: [PATCH 30/36] italian translation for new lang strings --- lang/it/customcert.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 lang/it/customcert.php diff --git a/lang/it/customcert.php b/lang/it/customcert.php new file mode 100644 index 00000000..0a8753e5 --- /dev/null +++ b/lang/it/customcert.php @@ -0,0 +1,31 @@ +. + +/** + * Language strings for the customcert module. + * + * @package mod_customcert + * @copyright 2013 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + $string['keeplocalcopy'] = 'Mantieni copia dei certificati'; + $string['keeplocalcopy_help'] = 'Se impostato a Sì, mantiene una copia del PDF dei certificati. La copia sarà scaricata ad ogni futura richiesta del certificato, finché il file non è cancellato dalla tabella files di moodle.'; + $string['deletelocalcopy'] = 'Cancella copia locale del certificato'; + $string['bulkdownloadlink'] = 'Download certificati'; + $string['deletelocalcopyconfirm'] = 'Sei sicuro di voler cancellare il file PDF del certificato?'; + $string['customcert:managekeeplocalcopy'] = 'Gestire impostazioni copia locale certificati'; + $string['customcert:deletelocalcopy'] = 'Cancella copia locale dei certificati'; From 94501630ed3524a8047057d3b0ef78c1351d1c9e Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Mon, 16 Oct 2023 15:46:52 +0200 Subject: [PATCH 31/36] fix sendPDF method signature --- classes/localfile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/localfile.php b/classes/localfile.php index 112a651f..22cff578 100644 --- a/classes/localfile.php +++ b/classes/localfile.php @@ -141,11 +141,11 @@ public function deletePDF(?int $userid = null) { * Send the PDF to the browser or return it as a string. * * @param int $userid the id of the user whose certificate we want to view - * @param int $deliveryoption the delivery option of the customcert + * @param string $deliveryoption the delivery option of the customcert * @param bool $return Do we want to return the contents of the PDF? * @return string|void Can return the PDF in string format if specified. */ - public function sendPDF(?int $userid = NULL, int $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { + public function sendPDF(?int $userid = NULL, string $deliveryoption = certificate::DELIVERY_OPTION_DOWNLOAD, bool $return = false) { global $USER; if (empty($userid)) { From 6b683ceee3eabf9cbd6d6442ab834a8ff2ec3a08 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Tue, 17 Oct 2023 11:43:34 +0200 Subject: [PATCH 32/36] add buildFileName static method --- classes/localfile.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/classes/localfile.php b/classes/localfile.php index 22cff578..e4102bd1 100644 --- a/classes/localfile.php +++ b/classes/localfile.php @@ -231,7 +231,19 @@ private static function buildFileInfoArr ($cm, ?int $userid = null, ?int $templa 'userid' => $USER->id, 'author' => fullname($USER), 'filepath' => '/' . $course->id . '/', - 'filename' => $user_info->username . '_cert-' . $templateid . '_course-' . $course->shortname . '.pdf' + 'filename' => self::buildFileName($user_info->username, $templateid, $course->shortname), ]; } + + /** + * Build the PDF filename. + * + * @param string $username + * @param string $templateid + * @param string $courseShortname + * @return string the PDF file name + */ + public static function buildFileName($username, $templateid, $courseShortname) { + return $username . '_cert-' . $templateid . '_course-' . $courseShortname . '.pdf'; + } } From 69f9dac2d0be3bb5decd8d823b2910c14478f8ad Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Tue, 17 Oct 2023 11:44:32 +0200 Subject: [PATCH 33/36] fix to download certs when keeplocalcopy is false --- downloadcerts.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/downloadcerts.php b/downloadcerts.php index f2d28d85..9d9dc981 100644 --- a/downloadcerts.php +++ b/downloadcerts.php @@ -36,6 +36,7 @@ die(); } +/** @var \moodle_database $DB */ global $DB; // Increase the server timeout to handle the creation and sending of large zip files. @@ -54,13 +55,15 @@ $context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $courseid]); $users = $DB->get_records('role_assignments', ['contextid' => $context->id]); +list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($u) => $u->userid, $users), SQL_PARAMS_NAMED); +$usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); // Build a list of files to zip. $filesforzipping = []; foreach ($certs as $certid => $cert_fields) { $template = null; - foreach ($users as $userid => $user_fields) { + foreach ($users as $roleAssignmentId => $user_fields) { if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { continue; } @@ -73,11 +76,18 @@ // must generate the pdf $pdf = $template->generate_pdf(false, $user_fields->userid, true); if (!empty($pdf)) { - $file = $lf->getPDF($user_fields->userid); + if ($cert_fields->keeplocalcopy) { + $file = $lf->getPDF($user_fields->userid); + } else { + $file = [ + 'content' => $pdf, + ]; + } } } if ($file) { - $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' .$file->get_filename()] = $file; + $filename = \mod_customcert\localfile::buildFileName($usersObjs[$user_fields->userid]->username, $template->get_id(), $course->shortname); + $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' . $filename] = $file; } } } From a813f8e9b5401220b9955610982587bd5d616d19 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Mon, 20 Nov 2023 12:53:12 +0100 Subject: [PATCH 34/36] bug fix and obey keeplocalcopy setting --- downloadcerts.php | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/downloadcerts.php b/downloadcerts.php index 9d9dc981..d6779317 100644 --- a/downloadcerts.php +++ b/downloadcerts.php @@ -21,7 +21,7 @@ * * @package mod_customcert * @author Gonzalo Romero - * @author Giorgio Consorti + * @author Giorgio Consorti * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -53,31 +53,23 @@ unset($cert); } -$context = $DB->get_record('context', ['contextlevel' => '50', 'instanceid' => $courseid]); -$users = $DB->get_records('role_assignments', ['contextid' => $context->id]); -list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($u) => $u->userid, $users), SQL_PARAMS_NAMED); -$usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); - // Build a list of files to zip. $filesforzipping = []; foreach ($certs as $certid => $cert_fields) { - $template = null; - foreach ($users as $roleAssignmentId => $user_fields) { - if (!$DB->get_record('customcert_issues', ['userid' => $user_fields->userid, 'customcertid' => $certid])) { - continue; - } - if (is_null($template)) { - $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); - $template = new \mod_customcert\template($template); - } - $lf = new \mod_customcert\localfile($template); - if (false === $file = $lf->getPDF($user_fields->userid)) { + $issues = $DB->get_records('customcert_issues', ['customcertid' => $certid]); + list($userssql, $params) = $DB->get_in_or_equal(array_map(fn($i) => $i->userid, $issues), SQL_PARAMS_NAMED); + $usersObjs = $DB->get_records_select('user', "id {$userssql}", $params); + $template = $DB->get_record('customcert_templates', ['id' => $cert_fields->templateid], '*', MUST_EXIST); + $template = new \mod_customcert\template($template); + $lf = new \mod_customcert\localfile($template); + foreach ($issues as $issue) { + if (false === $file = $lf->getPDF($issue->userid)) { // must generate the pdf - $pdf = $template->generate_pdf(false, $user_fields->userid, true); + $pdf = $template->generate_pdf(false, $issue->userid, true); if (!empty($pdf)) { if ($cert_fields->keeplocalcopy) { - $file = $lf->getPDF($user_fields->userid); + $file = $lf->getPDF($issue->userid); } else { $file = [ 'content' => $pdf, @@ -86,7 +78,7 @@ } } if ($file) { - $filename = \mod_customcert\localfile::buildFileName($usersObjs[$user_fields->userid]->username, $template->get_id(), $course->shortname); + $filename = \mod_customcert\localfile::buildFileName($usersObjs[$issue->userid]->username, $template->get_id(), $course->shortname); $filesforzipping['/' . $course->shortname . '/' . $cert_fields->name . '/' . $filename] = $file; } } From 369e8879f54acdccc88f67333cf2732f54c3766b Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Sat, 3 Feb 2024 13:16:34 +0100 Subject: [PATCH 35/36] fix minor bug in customcert_extend_navigation_course --- lib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib.php b/lib.php index a97b7818..260bb7ca 100644 --- a/lib.php +++ b/lib.php @@ -325,7 +325,7 @@ function customcert_extend_navigation_course(\navigation_node $parentnode, \stdC $addnode = $context->contextlevel === 50; $addnode = $addnode && !($context->instanceid === SITEID); $addnode = $addnode && has_capability('mod/customcert:viewallcertificates', $context); - $isCourseNav = is_null($PAGE->cm->instance); + $isCourseNav = !is_null($PAGE->cm) && is_null($PAGE->cm->instance); if ($addnode && $isCourseNav) { if ($node = build_downloadall_node($isCourseNav, $course, $context)) { $parentnode->add_node($node); From a83f1b69e994e6f4ade49231f07b5f29cdb7e426 Mon Sep 17 00:00:00 2001 From: Giorgio Consorti Date: Sat, 3 Feb 2024 13:17:04 +0100 Subject: [PATCH 36/36] remove duplicate delete_records call --- view.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/view.php b/view.php index f66d401f..19f327f0 100644 --- a/view.php +++ b/view.php @@ -93,11 +93,9 @@ exit(); } - // Delete the issue. - $DB->delete_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); // Always delete local copy. if ($candeletelocalcopy) { - $issues = $DB->get_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + $issues = $DB->get_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); if (!empty($issues)) { $lf = new \mod_customcert\localfile(new \mod_customcert\template($template)); array_map(fn($issue) => $lf->deletePDF($issue->userid), $issues); @@ -106,7 +104,7 @@ if (!$deletelocalcopy) { // Delete the issue. - $DB->delete_records('customcert_issues', array('id' => $deleteissue, 'customcertid' => $customcert->id)); + $DB->delete_records('customcert_issues', ['id' => $deleteissue, 'customcertid' => $customcert->id]); } // Redirect back to the manage templates page.