From 0d2de82fdcab1a0f97051bbe1e3d7faf48c1fcf2 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 22 Oct 2023 08:41:16 +0200 Subject: [PATCH] first commit --- README.md | 3 + classes/task/sync_enrolments_task.php | 81 ++ cli/sync.php | 105 ++ db/install.php | 24 + db/tasks.php | 37 + db/upgrade.php | 23 + lang/de/enrol_iserv.php | 98 ++ lang/en/enrol_iserv.php | 97 ++ lib.php | 1344 +++++++++++++++++++++++++ settings.php | 342 +++++++ settings_callbacks.php | 90 ++ version.php | 40 + 12 files changed, 2284 insertions(+) create mode 100644 README.md create mode 100644 classes/task/sync_enrolments_task.php create mode 100644 cli/sync.php create mode 100644 db/install.php create mode 100644 db/tasks.php create mode 100644 db/upgrade.php create mode 100644 lang/de/enrol_iserv.php create mode 100644 lang/en/enrol_iserv.php create mode 100644 lib.php create mode 100644 settings.php create mode 100644 settings_callbacks.php create mode 100644 version.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fdbb77 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## Moodle IServ Enrolments ## + +This Moodle plugin takes IServ groups and roles and creates Moodle courses and roles based on user configurable LDAP filters and attributes \ No newline at end of file diff --git a/classes/task/sync_enrolments_task.php b/classes/task/sync_enrolments_task.php new file mode 100644 index 0000000..d4d3852 --- /dev/null +++ b/classes/task/sync_enrolments_task.php @@ -0,0 +1,81 @@ +. + +/** + * IServ enrolment plugin implementation + * + * This plugin synchronizes courses and their enrolments with an IServ school server. + * Based partially on the OSS plugin by Frank Schütte + * + * @package enrol + * @subpackage iserv + * @author Jonas Lührig based on code by Frank Schütte based on code by Iñaki Arenaza + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @copyright 2010 Iñaki Arenaza + * @copyright 2020 Frank Schütte + * @copyright 2023 Gruelag GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace enrol_iserv\task; + +defined('MOODLE_INTERNAL') || die; + +/** + * Scheduled task to synchronize courses and enrolments + */ +class sync_enrolments_task extends \core\task\scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name () { + return get_string('sync_task_name', 'enrol_iserv'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + require_once ("{$CFG->dirroot}/enrol/iserv/lib.php"); + + // Ensure required plugins are enabled + if (! is_enabled_auth ("ldap")) { + debugging ("[AUTH LDAP] " . get_string ("pluginnotenabled", "auth_ldap")); + die; + } + + if (! enrol_is_enabled ("iserv")) { + debugging ("[ENROL IServ] " . get_string ("pluginnotenabled", "enrol_iserv")); + die; + } + + // Sync courses + $enrol = enrol_get_plugin ("iserv"); + if (! $enrol -> sync_courses ()) { + return false; + }; + + return true; + + } + +} diff --git a/cli/sync.php b/cli/sync.php new file mode 100644 index 0000000..c120ef1 --- /dev/null +++ b/cli/sync.php @@ -0,0 +1,105 @@ +. + +/** + * CLI sync for full OSS synchronisation. + * + * This script is meant to be called from a cronjob to sync moodle with the OSS + * server to pickup groups as moodle global groups (cohorts). + * + * Sample cron entry: + * # 5 minutes past every full hour + * 5 * * * * $sudo -u www-data /usr/bin/php /var/www/moodle/enrol/oss/cli/sync.php + * + * Notes: + * - it is required to use the web server account when executing PHP CLI scripts + * - you need to change the "www-data" to match the apache user account + * - use "su" if "sudo" not available + * - If you have a large number of users, you may want to raise the memory limits + * by passing -d momory_limit=256M + * - For debugging & better logging, you are encouraged to use in the command line: + * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 + * + * @package enrol + * @subpackage oss + * @author Frank Schütte - test script + * @copyright 2012 Frank Schütte + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define("CLI_SCRIPT", true); + +require (dirname(dirname(dirname(dirname(__FILE__)))) . "/config.php"); +require_once ("{$CFG -> libdir}/clilib.php"); + +// Parse CLI arguments +list ($options, $unrecognized) = cli_get_params ( + [ + "help" => false, + "user" => false + ], + [ + "h" => "help", + "u" => "user" + ], +); + +if ($unrecognized) { + $unrecognized = implode ("\n ", $unrecognized); + cli_error (get_string ("cliunknownoption", "enrol_iserv", $unrecognized)); +} else if ($options["help"]) { + $help = <<< EOH + Manually do a sync of IServ groups as courses. + Please ensure to enable and configure the enrol_iserv Plugin beforehand. + + Options: + -h, --help Print out this help + + Example: + \$sudo -u www-data /usr/bin/php enrol/iserv/cli/sync.php + + Notice: + Do not call this file through the system cron scheduler, rather + use the scheduled task within Moodle this plugin provides. + + + EOH; + + echo $help; + die; +} + +// Enable debug messages +$CFG -> debug = DEBUG_NORMAL; + +// Ensure required plugins are enabled +if (! is_enabled_auth ("ldap")) { + print ("[AUTH LDAP] " . get_string ("pluginnotenabled", "auth_ldap")); + die; +} + +if (! enrol_is_enabled ("iserv")) { + print ("[ENROL IServ] " . get_string ("pluginnotenabled", "enrol_iserv")); + die; +} + +// Sync courses +$enrol = enrol_get_plugin ("iserv"); +if (! $enrol -> sync_courses ($options["user"] ?? "*")) { + cli_error (get_string ("sync_failed", "enrol_iserv"), 1); +}; + +exit(0); \ No newline at end of file diff --git a/db/install.php b/db/install.php new file mode 100644 index 0000000..a60dd95 --- /dev/null +++ b/db/install.php @@ -0,0 +1,24 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_enrol_iserv_install() { + global $CFG, $DB; + + require_once($CFG->libdir . '/accesslib.php'); + return true; +} diff --git a/db/tasks.php b/db/tasks.php new file mode 100644 index 0000000..fc38ace --- /dev/null +++ b/db/tasks.php @@ -0,0 +1,37 @@ +. + +/** + * Definition of oss enrolment scheduled tasks. + * + * @package enrol_iserv + * @copyright 2023 Jonas Lührig + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$tasks = array( + array( + 'classname' => '\enrol_iserv\task\sync_enrolments_task', + 'blocking' => 0, + 'minute' => '53', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), +); diff --git a/db/upgrade.php b/db/upgrade.php new file mode 100644 index 0000000..fb2d132 --- /dev/null +++ b/db/upgrade.php @@ -0,0 +1,23 @@ +. + +function xmldb_enrol_iserv_upgrade($oldversion) { + global $CFG, $DB; + require_once($CFG->libdir . '/accesslib.php'); + + return true; +} diff --git a/lang/de/enrol_iserv.php b/lang/de/enrol_iserv.php new file mode 100644 index 0000000..febc3a3 --- /dev/null +++ b/lang/de/enrol_iserv.php @@ -0,0 +1,98 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +$string['enrolname'] = 'iserv'; +$string['pluginname'] = "IServ Einschreibungen"; +$string['pluginnotenabled'] = "Plugin {$string['pluginname']} ist nicht aktiviert"; +$string['cliunknownoption'] = "Unbekannte CLI Parameter:\n\n{$a}"; +$string['sync_failed'] = "Fehler bei der Synchronisation!"; +$string['sync_task_name'] = "Kurse und Einschreibungen synchronisieren"; + +/* Settings translations */ +$string['plugin_description'] = "Dieses Plugin synchronisiert IServ Gruppen als Moodle Kurse."; + +// Group Settings +$string['group_settings'] = "Gruppeneinstellungen"; +$string['group_settings_desc'] = "LDAP Einstellungen zu IServ Gruppen"; +$string['group_contexts_key'] = "Gruppenkontexte"; +$string['group_contexts_desc'] = "LDAP Kontexte, in denen Gruppen gefunden werden können. Mehrere Kontexte werden mit ; getrennt"; +$string['group_object_class_key'] = "Gruppen Objektklasse"; +$string['group_object_class_desc'] = "Objektklasse von Gruppen im LDAP Baum"; +$string['group_attribute_key'] = "Gruppen ID-Attribut"; +$string['group_attribute_desc'] = "LDAP Attribut, welches eine Gruppe identifiziert"; +$string['group_fullname_attribute_key'] = "Gruppen Namensattribut"; +$string['group_fullname_attribute_desc'] = "LDAP Attribut, welches den vollen Namen einer Gruppe enthält"; +$string['group_member_attribute_key'] = "Gruppen Mitgliedsattribut"; +$string['group_member_attribute_desc'] = "LDAP Attribut, welches ein Mitglied einer Gruppe darstellt"; +$string['group_member_attribute_is_dn_key'] = "Gruppen Mitgliedsattribut ist DN"; +$string['group_member_attribute_is_dn_desc'] = "Gibt an, ob das Gruppen Mitgliedsattribut ein DN Wert ist"; +$string['group_search_subtree_key'] = "Im gesamten Baum suchen"; +$string['group_search_subtree_desc'] = "Gibt an, ob im gesamten Baum unter den Gruppenkontexten nach Gruppen gesucht werden soll"; + +// Role Settings +$string['role_settings'] = "IServ Rollen-Einstellungen"; +$string['role_settings_desc'] = "LDAP Einstellungen zu IServ Rollen"; +$string['role_contexts_key'] = "Rollenkontexte"; +$string['role_contexts_desc'] = "LDAP Kontexte, in denen Rollen gefunden werden können. Mehrere Kontexte werden mit ; getrennt."; +$string['role_object_class_key'] = "Rollen Objektklasse"; +$string['role_object_class_desc'] = "Objektklasse von Rollen im LDAP Baum"; +$string['teachers_role_name_key'] = "Rolle Lehrkräfte"; +$string['teachers_role_name_desc'] = "Rollenname von Lehrkräften"; +$string['students_role_name_key'] = "Rolle Schüler/innen"; +$string['students_role_name_desc'] = "Rollenname von Schüler/innen"; +$string['role_member_attribute_key'] = "Rollen Mitgliedsattribut"; +$string['role_member_attribute_desc'] = "LDAP Attribut, welches ein Mitglied einer Rolle darstellt"; +$string['role_member_attribute_is_dn_key'] = "Rollen Mitgliedsattribut ist DN"; +$string['role_member_attribute_is_dn_desc'] = "Gibt an, ob das Rollen Mitgliedsattribut ein DN Wert ist"; + +// Course Mapping +$string['coursemapping'] = "Kurszuweisung"; +$string['coursemapping_desc'] = "Einstellungen zur Zuweisung von IServ Gruppen zu Moodle Kursen"; +$string['coursemapping_attribute_key'] = "Kurs Attribut"; +$string['coursemapping_attribute_desc'] = "LDAP Attribut, wessen Wert eine Gruppe als Kurs markiert"; +$string['coursemapping_attribute_value_key'] = "Kurs Attributwert"; +$string['coursemapping_attribute_value_desc'] = "Wert des obrigen Kurs Attributes"; +$string['coursemapping_use_attribute_key'] = "Kurs Attribut benutzen"; +$string['coursemapping_use_attribute_desc'] = "Gibt an, ob das Kurs Attribut verwendet werden soll, um Gruppen Kursen zuzuweisen"; +$string['coursemapping_prefixes_key'] = "Kurs CN Präfix"; +$string['coursemapping_prefixes_desc'] = "Ein Präfix im CN Gruppen Attribut, welches eine Gruppe als Moodle Kurs markiert. Ein * wird als Platzhalter für den restlichen CN Wert eingesetzt"; +$string['coursemapping_use_prefixes_key'] = "Kurs CN Präfix verwenden"; +$string['coursemapping_use_prefixes_desc'] = "Gibt an, ob das Kurs CN Präfix verwendet werden soll, um Gruppen Kursen zuzuweisen. Hinweis: Das Kurs Attribut hat Vorrang, wenn beide aktiv sind!"; + +// Course Settings +$string['courses'] = "Kurseinstellungen"; +$string['courses_desc'] = "Einstellungen zu Moodle Kursen"; +$string['courses_category_key'] = "Kurskategorie Name"; +$string['courses_category_desc'] = "Name der Kurskategorie, in welcher neue Kurse erstellt werden"; +$string['courses_category_autocreate_key'] = "Kurskategorie automatisch erstellen"; +$string['courses_category_autocreate_desc'] = "Gibt an, ob die Kurskategorie automatisch erstellt werden soll"; +$string['courses_autocreate_key'] = "Kurse automatisch erstellen"; +$string['courses_autocreate_desc'] = "Gibt an, ob Kurse automatisch erstellt werden sollen"; +$string['courses_autoremove_key'] = "Kurse automatisch löschen"; +$string['courses_autoremove_desc'] = "Gibt an, ob Kurse automatisch gelöscht werden sollen, wenn diese in IServ nicht mehr existieren oder der Kurs keine Mitglieder mehr besitzt"; +$string['courses_template_key'] = "Kurs Vorlage"; +$string['courses_template_desc'] = "Vorhandener Kurs, welcher als Vorlage für neue Kurse genutzt werden soll"; +$string['course_template_none'] = "Keine Vorlage"; +$string['courses_teacher_role_key'] = "Rolle Lehrkräfte"; +$string['courses_teacher_role_desc'] = "Rolle, welche Lehrkräften zugeweisen wird"; +$string['courses_student_role_key'] = "Rolle Schüler/innen"; +$string['courses_student_role_desc'] = "Rolle, welche Schüler/innen zugeweisen wird"; + +/* Internal strings */ +$string['courses_category_description'] = "Kurse von IServ importiert"; \ No newline at end of file diff --git a/lang/en/enrol_iserv.php b/lang/en/enrol_iserv.php new file mode 100644 index 0000000..298e16a --- /dev/null +++ b/lang/en/enrol_iserv.php @@ -0,0 +1,97 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +$string['enrolname'] = 'iserv'; +$string['pluginname'] = "IServ Enrolments"; +$string['pluginnotenabled'] = "Plugin {$string['pluginname']} is not enabled"; +$string['cliunknownoption'] = "Unknown CLI parameter:\n\n{$a}"; +$string['sync_failed'] = "Error during the synchronization process!"; +$string['sync_task_name'] = "Synchronize courses and enrolments"; + +/* Settings translations */ +$string['plugin_description'] = "This plugin synchronizes IServ roles via LDAP as Moodle courses"; + +// Group Settings +$string['group_settings'] = "IServ Group LDAP Settings"; +$string['group_settings_desc'] = "Settings related to LDAP groups"; +$string['group_contexts_key'] = "Group Contexts"; +$string['group_contexts_desc'] = "LDAP contexts in which groups can be found. Multiple contexts are separated by a semi-colon (;)"; +$string['group_object_class_key'] = "Group Object Class"; +$string['group_object_class_desc'] = "Object class of groups in LDAP tree"; +$string['group_attribute_key'] = "Group ID Attribute"; +$string['group_attribute_desc'] = "LDAP attribute which identifies a group"; +$string['group_fullname_attribute_key'] = "Group Display Name Attribute"; +$string['group_fullname_attribute_desc'] = "LDAP attribute which contains the display name of a group"; +$string['group_member_attribute_key'] = "Group Member Attribute"; +$string['group_member_attribute_desc'] = "LDAP attribute which specifies a group member"; +$string['group_member_attribute_is_dn_key'] = "Group Member Attribute is DN"; +$string['group_member_attribute_is_dn_desc'] = "Specifies if the group member attribute is a DN value"; +$string['group_search_subtree_key'] = "Search Sub-Trees"; +$string['group_search_subtree_desc'] = "When true, groups are searched in sub-trees of the group contexts"; + +// Role Settings +$string['role_settings'] = "IServ Role LDAP Settings"; +$string['role_settings_desc'] = "Settings related to IServ roles"; +$string['role_contexts_key'] = "Role Contexts"; +$string['role_contexts_desc'] = "LDAP contexts in which roles can be found. Multiple contexts are separated by a semi-colon (;)."; +$string['role_object_class_key'] = "Role Object Class"; +$string['role_object_class_desc'] = "Object class of roles in LDAP tree"; +$string['teachers_role_name_key'] = "Teachers Role"; +$string['teachers_role_name_desc'] = "Role name of teachers"; +$string['students_role_name_key'] = "Students Role"; +$string['students_role_name_desc'] = "Role name of students"; +$string['role_member_attribute_key'] = "Role Member Attribute"; +$string['role_member_attribute_desc'] = "LDAP attribute which specifies a role member"; +$string['role_member_attribute_is_dn_key'] = "Role membership attribute is DN"; +$string['role_member_attribute_is_dn_desc'] = "Specifies if the role membership attribute is a DN value"; + +// Course Mapping +$string['coursemapping'] = "Course Mapping"; +$string['coursemapping_desc'] = "Settings specifying how IServ groups are mapped to Moodle courses"; +$string['coursemapping_attribute_key'] = "Course Attribute"; +$string['coursemapping_attribute_desc'] = "LDAP attribute whose value marks IServ groups as Moodle courses"; +$string['coursemapping_attribute_value_key'] = "Course Attribute Value"; +$string['coursemapping_attribute_value_desc'] = "Value of the course attribute above"; +$string['coursemapping_use_attribute_key'] = "Use Course Attribute"; +$string['coursemapping_use_attribute_desc'] = "Specifies if the course attribute is used for mapping"; +$string['coursemapping_prefixes_key'] = "Course CN Prefix"; +$string['coursemapping_prefixes_desc'] = "A prefix in the group CN attribute which marks IServ groups as Moodle courses. An asterisk (*) is used as placeholder for remaining text in the CN attribute"; +$string['coursemapping_use_prefixes_key'] = "Use course CN prefix"; +$string['coursemapping_use_prefixes_desc'] = "Specifies if the course CN prefix is used for mapping. Notice: The course attribute takes precedence when both are active!"; + +// Course Settings +$string['courses'] = "Course Settings"; +$string['courses_desc'] = "Settings related to Moodle courses"; +$string['courses_category_key'] = "Courses category name"; +$string['courses_category_desc'] = "Name of the courses category in which any mapped courses will be organized"; +$string['courses_category_autocreate_key'] = "Autocreate Courses Category"; +$string['courses_category_autocreate_desc'] = "Specifies if the courses category should be auto created"; +$string['courses_autocreate_key'] = "Autocreate Courses"; +$string['courses_autocreate_desc'] = "Specifies if courses should be auto created"; +$string['courses_autoremove_key'] = "Autoremove Courses"; +$string['courses_autoremove_desc'] = "Specifies if courses shold be auto removed if the group gets removed from IServ or the group has no members left"; +$string['courses_template_key'] = "Course Template"; +$string['courses_template_desc'] = "Existing course to use as template for new courses"; +$string['course_template_none'] = "No template"; +$string['courses_teacher_role_key'] = "Teachers role"; +$string['courses_teacher_role_desc'] = "Course role to assign to teachers"; +$string['courses_student_role_key'] = "Students role"; +$string['courses_student_role_desc'] = "Course role to assign to students"; + +/* Internal strings */ +$string['courses_category_description'] = "Courses imported from IServ"; \ No newline at end of file diff --git a/lib.php b/lib.php new file mode 100644 index 0000000..1c47b60 --- /dev/null +++ b/lib.php @@ -0,0 +1,1344 @@ +. + +/** + * IServ enrolment plugin implementation + * + * This plugin synchronizes courses and their enrolments with an IServ school server. + * Based partially on the OSS plugin by Frank Schütte + * + * @package enrol + * @subpackage iserv + * @author Jonas Lührig based on code by Frank Schütte based on code by Iñaki Arenaza + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @copyright 2010 Iñaki Arenaza + * @copyright 2020 Frank Schütte + * @copyright 2023 Gruelag GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +class enrol_iserv_plugin extends enrol_plugin { + protected $enroltype = 'enrol_iserv'; + static protected $error_log_tag = 'ENROL ISERV'; + static protected $idnumber_course_category = 'iserv_courses'; + protected $teacher_array = array(); + protected $auth_ldap; + + // Message logging modes + const LOG_NORMAL = 0; + const LOG_DEBUG = 1; + const LOG_MTRACE = 2; + + public function __construct() { + global $CFG; + require_once($CFG->libdir . '/accesslib.php'); + require_once($CFG->libdir . '/ldaplib.php'); + require_once($CFG->libdir . '/moodlelib.php'); + require_once($CFG->libdir . '/enrollib.php'); + require_once($CFG->libdir . '/dml/moodle_database.php'); + require_once($CFG->dirroot . '/group/lib.php'); + require_once($CFG->dirroot . '/auth/ldap/auth.php'); + require_once($CFG->dirroot . '/course/lib.php'); + + $this->load_config(); + // Make sure we get sane defaults for critical values. + $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8'); + $this->config->user_type = $this->get_config('user_type', 'default'); + + $ldap_usertypes = ldap_supported_usertypes(); + $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; + unset($ldap_usertypes); + + $default = ldap_getdefaults(); + // Use defaults if values not given. Dont use this->get_config() + // here to be able to check for 0 and false values too. + foreach ($default as $key => $value) { + // Watch out - 0, false are correct values too, so we can't use $this->get_config(). + if (!isset($this->config->{$key}) or $this->config->{$key} == '') { + $this->config->{$key} = $value[$this->config->user_type]; + } + } + } + + /** + * Custom message logging function + * @param string $text Message text to be logged + * @param string $func Optional: Current function the message is logged from + * @param bool $mode Optional: Specify logging mode, see LOG_ constants in class + */ + static function debuglog ($text, $func = "Generic", $mode = 0) { + $error_log_tag = self::$error_log_tag; + $now = date ("H:i:s"); + $line = "[{$error_log_tag} -> {$func} @ {$now}] {$text}"; + + if (defined("CLI_SCRIPT")) $mode = -1; + + switch ($mode) { + case -1: + print ($line . PHP_EOL); + break; + case self::LOG_DEBUG: + debugging ($line, DEBUG_DEVELOPER); + break; + case self::LOG_MTRACE: + mtrace ($line); + break; + default: + debugging ($line); + } + } + + /** + * Get courses from LDAP and sync them with Moodle courses, then + * ensure users are properly enrolled into those courses + * + * @param string $userid User ID to sync enrolments with + */ + public function sync_courses ($userid = "*") { + if ($this -> config -> courses_autocreate || $this -> config -> courses_autoremove) { + self::debuglog ( + "Syncing courses for user {$userid}", + "sync_courses", + self::LOG_DEBUG + ); + + $ldap_courses = $this -> get_courses_ldap(); + $moodle_courses = $this -> get_courses_moodle(); + + if ($this -> config -> courses_autocreate) { + $to_add = array_diff_key ($ldap_courses, $moodle_courses); + + if (!empty($to_add)) { + $this -> create_courses ($to_add); + } + } + + if ($this -> config -> courses_autoremove) { + $to_remove = array_diff_key ($moodle_courses, $ldap_courses); + + if (!empty($to_remove)) { + $this -> remove_courses (array_keys($to_remove)); + } + } + } + + if ($userid && strcmp($userid,"*") !== 0) { + $this -> sync_course_enrolments_user ($userid); + } else { + $this -> sync_course_enrolments(); + } + + return true; + } + + /** + * create a list of class courses, whose names are given in an array + * + * @param array $classes + */ + private function create_courses ($courses = []) { + global $DB; + if (empty($courses)) return; + + $courses_category = self::get_courses_category($this->config); + if (!$courses_category) return; + + $template = $DB -> get_record ( + 'course', + array( + 'id' => $this -> config -> courses_template + ), + '*', + IGNORE_MISSING + ); + + if ($template) { + $template = $template->id; + } + + foreach ($courses as $shortname => $fullname) { + $this -> create_course ( + $shortname, + $courses_category -> id, + $fullname, + $template + ); + + $newcourses = true; + } + if ($newcourses) { + context_coursecat::instance ( $courses_category -> id ) -> mark_dirty (); + } + } + + /** + * Remove all courses given in array from course category + * + * @param array $courses Courses to remove + */ + private function remove_courses ($courses = array()) { + global $DB; + + if (empty($courses)) return; + + $courses_category = self::get_courses_category ($this->config); + if (!$courses_category) return; + + $ids = array (); + foreach ($courses as $course) { + $record = $DB -> get_record ( + 'course', + array ( + 'shortname' => $course, + 'category' => $courses_category -> id + ), + '*' + ); + + $ids[] = $record -> id; + } + + foreach ($ids as $id) { + $this -> remove_course ($id); + } + } + + /** + * Create a new course either as duplicate from $template or as new empty course + * + * @param string $name Short name of new course + * @param integer $category_id ID of parent category + * @param string $fullname Full name of new course + * @param number $template Template ID to clone new course from + * + * @return course object|false + */ + private function create_course ($name, $category_id, $fullname = null, $template = 0) { + global $CFG; + require_once ($CFG->dirroot . '/course/externallib.php'); + require_once ($CFG->dirroot . '/course/lib.php'); + + $course = false; + if ($fullname === null) $fullname = $course; + + self::debuglog( + "Creating course {$fullname} @{$name} in category {$category_id} with template {$template}", + "create_course" + ); + + if (!$template) { + $data = new stdclass(); + $data -> shortname = $name; + $data -> fullname = $fullname; + $data -> visible = 1; + $data -> category = $category_id; + + try { + $course = create_course($data); + self::debuglog( + "Course {$fullname} @{$name} successfully created", + "create_course", + self::LOG_MTRACE + ); + } catch (Exception $e) { + self::debuglog( + "Course {$fullname} @{$name} failed to create: " . $e->getMessage(), + "create_course" + ); + } + } else { + try { + $course = core_course_external::duplicate_course($template, $fullname, $name, $category_id, 1); + $course = get_course($course["id"], false); + + self::debuglog( + "Course {$fullname} @{$name} successfully duplicated from template", + "create_course", + self::LOG_MTRACE + ); + } catch ( Exception $e ) { + self::debuglog( + "Course {$fullname} @{$name} failed to duplicate: " . $e->getMessage(), + "create_course" + ); + } + } + + return $course; + } + + /** + * Remove a course + * + * @param int $course_id ID of course to remove + * + * @return true True on success + * @return false False on failure + */ + private function remove_course ($course_id) { + global $CFG; + require_once ($CFG->libdir . '/moodlelib.php'); + return delete_course($course_id); + } + + /** + * Search for courses based on LDAP groups + * + * @param string $userid Optional: ID of the user to get courses for + * + * @return array Associative array of internal group name and group description + * @return false False on failure + */ + private function get_courses_ldap ($userid = "*") { + // Create course filter + if ($this -> config -> coursemapping_use_prefixes) { + $prefixes = explode (',', $this -> config -> coursemapping_prefixes); + foreach ($prefixes as $prefix) { + $pattern[] = "({$this -> config -> group_attribute}={$prefix})"; + } + $pattern = '(|' . implode ($pattern) . ')'; + } else if ($this -> config -> coursemapping_use_attribute) { + $pattern = "({$this -> config -> coursemapping_attribute}={$this -> config -> coursemapping_attribute_value})"; + } else { + self::debuglog ( + "Invalid settings, enable either coursemapping_use_prefix or coursemapping_use_attribute", + "get_courses_ldap" + ); + return false; + } + + self::debuglog ( + "Using filter {$pattern} to fetch courses for {$userid}", + "get_courses_ldap", + self::LOG_DEBUG + ); + + return $this -> ldap_get_grouplist ($userid, $pattern); + } + + /** + * Returns an array of class \core_course_list_element objects for the $userid + * + * @param string $userid ID of user to get Moodle courses for + * + * @return array Course array of Moodle courses + * + * @uses $USER + */ + private function get_courses_moodle ($userid = "*") { + global $USER; + $return_var = array (); + + if ($userid == "*") { + $user = null; + } else { + $user = \core_user::get_user_by_username ($userid, 'id', null, IGNORE_MISSING); + if (!$user) { + self::debuglog ( + "Could not find user with ID {$userid}", + "get_courses_moodle" + ); + return $return_var; + } + } + + $courses_category = self::get_courses_category($this -> config); + if (!$courses_category) { + self::debuglog ( + "Courses category not found", + "get_courses_moodle" + ); + return $return_var; + } + + // Sloppy fix for something I haven't figured out yet, to get all courses, + // set $USER->id to 1, which means guest, otherwise no courses are returned + $previous_user_id = $USER -> id; + $USER -> id = 1; + + $courselist = $courses_category -> get_courses(); + foreach ($courselist as $record) { + if ($record -> visible) { + $context = context_course::instance($record->id); + if (is_null($user) || is_enrolled($context, $user, '', true)) { + $return_var [$record->shortname] = $record; + } + } + } + + $USER -> id = $previous_user_id; + return $return_var; + } + + /** + * Fetches and creates the class category if necessary + * + * @param object $config The configuration object from $this -> + * + * @return object Returns the category object on success + * @return false Returns false on failure + * + * @uses $DB + */ + private static function get_courses_category ($config) { + global $DB; + + // Fetch category object from DB + $category_obj = $DB -> get_record ( + 'course_categories', + array ( + 'idnumber' => self::$idnumber_course_category, + 'parent' => 0 + ), + 'id', + IGNORE_MULTIPLE + ); + + // Create class category if needed + if (! $category_obj) { + if (isset ($config -> courses_category_autocreate) && $config -> courses_category_autocreate) { + $category_obj = self::create_category ( + $config -> courses_category, + self::$idnumber_course_category, + get_string ('courses_category_description', 'enrol_iserv') + ); + + if ($category_obj) { + self::debuglog ( + "Created courses category {$category_obj -> id}", + "get_course_category", + self::LOG_DEBUG + ); + } else { + self::debuglog ( + "Could not autocreate courses category", + "get_course_category" + ); + return false; + } + } + } else { + self::debuglog ( + "Trying to get course category with ID {$category_obj -> id}", + "get_course_category", + self::LOG_DEBUG + ); + + $category_obj = \core_course_category::get ( + $category_obj -> id, + IGNORE_MISSING, + true + ); + } + + if (! $category_obj) { + self::debuglog ( + "Courses category " . self::$idnumber_course_category . " not found", + "get_course_category" + ); + return false; + } + + return $category_obj; + } + + /** + * Renames the courses category + * + * @param string $name New courses category name + * + * @param true True on success + * @param false False on failure + */ + public function rename_courses_category ($name) { + global $DB; + + if (! $name || $name == "") return false; + + self::debuglog ( + "Renaming courses category to {$name}", + "rename_courses_category", + self::LOG_DEBUG + ); + + $category = self::get_courses_category ($this->config); + + $result = $DB -> set_field ( + 'course_categories', + 'name', + $name, + array ('id' => $category->id) + ); + + if ($result) { + self::debuglog ( + "Success, marking category as dirty", + "rename_courses_category", + self::LOG_DEBUG + ); + + context_coursecat::instance ($category -> id) -> mark_dirty (); + return true; + } + + self::debuglog ( + "Renaming courses category failed", + "rename_courses_category", + self::LOG_DEBUG + ); + + return false; + } + + /** + * This function creates a course category and fixes the category path. + * @param string $name New category name + * @param string $idnumber New category idnumber + * @param string $description Descriptive text for the new course category + * @param object $parent course_categories parent object or 0 for top level category + * @param int $sortorder special sort order, 99999 order at end + * @param int $visible Specifies if the category is visible or hidden + * + * @return object Returns the new category object on success + * @return false Returns false on failure + * @uses $DB; + */ + private static function create_category ($name, $idnumber, $description, $parent = 0, $sortorder = 0, $visible = 1) { + global $DB; + + self::debuglog( + "Creating {$name} @{$idnumber} with sortorder ({$sortorder})", + "create_category", + self::LOG_DEBUG + ); + + $data = new stdClass(); + $data -> name = $name; + $data -> idnumber = $idnumber; + $data -> description = $description; + $data -> parent = $parent; + $data -> visible = $visible; + + $category = \core_course_category::create ($data); + + if (!$category) { + self::debuglog( + "Could not create new course category {$category->name} @{$category->idnumber}", + "create_category" + ); + return false; + } + + if ($sortorder != 0) { + self::debuglog ( + "Changing course sortorder to {$sortorder}", + "create_category", + self::LOG_DEBUG + ); + + $DB -> set_field ( + 'course_categories', + 'sortorder', + $sortorder, + array( + 'id' => $category->id + ) + ); + context_coursecat::instance ($category -> id) -> mark_dirty(); + fix_course_sortorder(); + } + + return $category; + } + + /** + * Get all groups from LDAP which match search criteria defined in settings + * + * A $group_pattern of the form "(|(cn=05*)(cn=06*)...)" can be provided, otherwise + * a default $group_pattern is generated. + * + * @param string $username Limits groups to those which username is a member of + * @param string $group_pattern LDAP filter pattern to filter groups + * + * @return array Associative array of interal group name and group description + * @return false False on failure + */ + private function ldap_get_grouplist($username = "*", $group_pattern = null) { + self::debuglog ( + "Started fetching groups for {$username}", + "ldap_get_grouplist", + self::LOG_DEBUG + ); + + $auth_ldap = $this -> auth_ldap; + if (!isset($auth_ldap) or empty($auth_ldap)) { + $this -> auth_ldap = $auth_ldap = get_auth_plugin('ldap'); + } + + self::debuglog ( + "LDAP connecting...", + "ldap_get_grouplist", + self::LOG_DEBUG + ); + + $ldapconnection = $auth_ldap->ldap_connect(); + if (!$ldapconnection) { + self::debuglog ( + "LDAP connection failed", + "ldap_get_grouplist" + ); + + return false; + } + + self::debuglog ( + "LDAP connected", + "ldap_get_grouplist", + self::LOG_DEBUG + ); + + // Create LDAP filter if username is specified + if ($username == "*") { + $filter = ""; + } else { + $filter = "({$this->config->group_member_attribute}={$username})"; + } + + // Use generic LDAP group pattern if none was provided + if (is_null ($group_pattern)) { + $group_pattern = $this -> ldap_generate_group_pattern (); + } + + $filter = "(&{$group_pattern}{$filter}(objectclass={$this -> config -> group_object_class}))"; + $contexts = explode(';', $this -> config -> group_contexts); + $found_groups = array (); + + // Iterate through all group contexts to look up any matching groups + foreach ($contexts as $context) { + $context = trim ($context); + if (empty ($context)) continue; + + if ($this -> config -> group_search_subtree) { + // Search groups in this context and subcontexts + $ldap_result = ldap_search ($ldapconnection, $context, $filter, array ( + $this -> config -> group_attribute + )); + } else { + // Search only in the current context + $ldap_result = ldap_list ($ldapconnection, $context, $filter, array ( + $this -> config -> group_attribute, + $this -> config -> group_fullname_attribute + )); + } + + $groups = ldap_get_entries($ldapconnection, $ldap_result); + + // Add found groups to found_groups array + for ($i = 0; $i < count($groups) - 1; $i++) { + $group_name = $groups[$i][$this -> config -> group_attribute][0]; + $group_desc = $groups[$i][$this -> config -> group_fullname_attribute][0]; + $found_groups[$group_name] = $group_desc; + } + } + + self::debuglog ( + "LDAP closing...", + "ldap_get_grouplist", + self::LOG_DEBUG + ); + $auth_ldap->ldap_close(); + self::debuglog ( + "LDAP closed...", + "ldap_get_grouplist", + self::LOG_DEBUG + ); + + return $found_groups; + } + + /** + * This function finds the user in the teacher array. + * + * @param string $userid ID of the user to check + */ + private function is_teacher($userid) { + self::debuglog ( + "Checking user {$userid}", + "is_teacher", + self::LOG_DEBUG + ); + + if (empty($userid)) { + self::debuglog ( + "Function called with empty userid", + "is_teacher" + ); + return false; + } + + if (empty($this->teacher_array)) { + $this->init_teacher_array(); + } + + return in_array($userid, $this->teacher_array); + } + + /** + * This function inits the teacher_array once + * + * @return true True on success + * @return false False on failure + */ + private function init_teacher_array() { + self::debuglog ( + "Initializing array", + "init_teacher_array", + self::LOG_DEBUG + ); + + $this -> teacher_array = $this -> ldap_get_role_members ($this -> config -> teachers_role_name, true); + + self::debuglog ( + "Finished", + "init_teacher_array", + self::LOG_DEBUG + ); + + if (empty($this -> teacher_array)) { + return false; + } + + return true; + } + + /** + * Search for group members on the IServ server within a specified group + * + * @param string $group Group to fetch members from + * + * @return string[] Array of users on success + * @return false False on failure + */ + private function ldap_get_group_members ($group) { + global $CFG, $DB; + + self::debuglog ( + "Fetching members for group {$group}...", + "ldap_get_group_members", + self::LOG_DEBUG + ); + + $return_var = array (); + $members = array (); + $auth_ldap = $this->auth_ldap; + + if (!isset($auth_ldap) or empty($auth_ldap)) { + $this->auth_ldap = $auth_ldap = get_auth_plugin('ldap'); + } + + self::debuglog ( + "LDAP connecting...", + "ldap_get_group_members", + self::LOG_DEBUG + ); + $ldapconnection = $auth_ldap->ldap_connect(); + + $group = core_text::convert($group, 'utf-8', $this -> config -> ldapencoding); + + if (!$ldapconnection) { + self::debuglog ( + "LDAP connection failed", + "ldap_get_group_members" + ); + return $return_var; + } + + self::debuglog ( + "LDAP connected", + "ldap_get_group_members", + self::LOG_DEBUG + ); + $group_query = "(&(cn=" . trim($group) . ")(objectClass={$this -> config -> group_object_class}))"; + $contexts = explode(';', $this -> config -> group_contexts); + + // Iterate trough all contexts and try to find the group there + foreach ($contexts as $context) { + $context = trim($context); + if (empty ($context)) continue; + + self::debuglog ( + "LDAP Search in {$context} filters {$group_query}", + "ldap_get_group_members", + self::LOG_DEBUG + ); + $ldap_result = ldap_search ($ldapconnection, $context, $group_query); + + if (!empty ($ldap_result) AND ldap_count_entries ($ldapconnection, $ldap_result)) { + self::debuglog ( + "Fetching entries with ldap_get_entries...", + "ldap_get_group_members", + self::LOG_DEBUG + ); + + $entries = ldap_get_entries($ldapconnection, $ldap_result); + $group_member_attribute = strtolower ($this -> config -> group_member_attribute); + + // Iterate over all matching groups in the current context and collect their members + $totalMembers = 0; + foreach ($entries as $entry) { + if (isset ($entry[$group_member_attribute])) { + $memberCountInThisContext = count ($entry[$group_member_attribute]); + + // Iterate through all members + for ($g = 0; $g < ($memberCountInThisContext - 1); $g++) { + $member = trim ($entry[$group_member_attribute][$g]); + + // Skip blank members + if ($member == "") continue; + + // Grab CN from DN if necessary + if ($this -> config -> group_member_attribute_is_dn) { + if (! $member = $this -> get_cn_from_dn ($member)) { + self::debuglog ( + "Failed to fetch CN from DN '{$member}', check setting role_member_attribute_is_dn!", + "ldap_get_group_members" + ); + } + } + + // Add member to members array if they're not already part of it + if (! in_array ($member, $members)) { + $members[] = $member; + $totalMembers ++; + + self::debuglog ( + "Found member {$member}", + "ldap_get_group_members", + self::LOG_DEBUG + ); + } + } + } + } + + self::debuglog ( + "Number of members found for {$this -> config -> group_member_attribute}: {$totalMembers}" , + "ldap_get_group_members", + self::LOG_DEBUG + ); + } + } + + self::debuglog ( + "LDAP closing...", + "ldap_get_group_members", + self::LOG_DEBUG + ); + $auth_ldap->ldap_close(); + self::debuglog ( + "LDAP closed", + "ldap_get_group_members", + self::LOG_DEBUG + ); + + // Generate SELECT statement for DB call + foreach ($members as $member) { + if (isset($select)) { + $select = "{$select},'{$member}'"; + } else { + $select = "'{$member}'"; + } + } + + // Fetch users from DB to match IDs to usernames + if (isset($select)) { + $select = "username IN ({$select})"; + $members = $DB -> get_recordset_select('user', $select, null, null, 'id,username'); + foreach ($members as $member) { + $return_var[$member -> id] = $member -> username; + } + } else { + self::debuglog ( + "No users found", + "ldap_get_group_members" + ); + } + return $return_var; + } + + /** + * Get all users that are member of an IServ role + * + * @param string $role Role to fetch members from + * + * @return string[] Array of users on success + * @return false False on failure + */ + private function ldap_get_role_members ($role) { + global $CFG, $DB; + + self::debuglog ( + "Fetching members for role {$role}...", + "ldap_get_role_members", + self::LOG_DEBUG + ); + + $return_var = array (); + $members = array (); + $auth_ldap = $this->auth_ldap; + + if (!isset($auth_ldap) or empty($auth_ldap)) { + $this->auth_ldap = $auth_ldap = get_auth_plugin('ldap'); + } + + self::debuglog ( + "LDAP connecting...", + "ldap_get_role_members", + self::LOG_DEBUG + ); + $ldapconnection = $auth_ldap->ldap_connect(); + + $role = core_text::convert($role, 'utf-8', $this -> config -> ldapencoding); + + if (!$ldapconnection) { + self::debuglog ( + "LDAP connection failed", + "ldap_get_role_members" + ); + return $return_var; + } + + self::debuglog ( + "LDAP connected", + "ldap_get_role_members", + self::LOG_DEBUG + ); + + $role = trim($role); + $role_query = "(&(cn={$role})(objectClass={$this -> config -> role_object_class}))"; + $contexts = explode(';', $this -> config -> role_contexts); + + // Iterate trough all contexts and try to find the role there + foreach ($contexts as $context) { + $context = trim($context); + if (empty ($context)) continue; + + self::debuglog ( + "LDAP Search in {$context} filters {$role_query}", + "ldap_get_role_members", + self::LOG_DEBUG + ); + $ldap_result = ldap_search ($ldapconnection, $context, $role_query); + + if (!empty ($ldap_result) AND ldap_count_entries ($ldapconnection, $ldap_result)) { + self::debuglog ( + "Fetching entries with ldap_get_entries...", + "ldap_get_role_members", + self::LOG_DEBUG + ); + + $entries = ldap_get_entries($ldapconnection, $ldap_result); + $role_member_attribute = strtolower ($this -> config -> role_member_attribute); + + // Iterate over all matching roles in the current context and collect their members + $totalMembers = 0; + foreach ($entries as $entry) { + if (isset ($entry[$role_member_attribute])) { + $memberCountInThisContext = count ($entry[$role_member_attribute]); + + // Iterate through all members + for ($g = 0; $g < ($memberCountInThisContext - 1); $g++) { + $member = trim ($entry[$role_member_attribute][$g]); + + // Skip blank members + if ($member == "") continue; + + // Grab CN from DN if necessary + if ($this -> config -> role_member_attribute_is_dn) { + if (! $member = $this -> get_cn_from_dn ($member)) { + self::debuglog ( + "Failed to fetch CN from DN '{$member}', check setting role_member_attribute_is_dn!", + "ldap_get_role_members" + ); + } + } + + // Add member to members array if they're not already part of it + if (! in_array ($member, $members)) { + $members[] = $member; + $totalMembers ++; + + self::debuglog ( + "Found member {$member}", + "ldap_get_role_members", + self::LOG_DEBUG + ); + } + } + } + } + + self::debuglog ( + "Number of members found for {$this -> config -> role_member_attribute}: {$totalMembers}" , + "ldap_get_role_members", + self::LOG_DEBUG + ); + } + } + + self::debuglog ( + "LDAP closing...", + "ldap_get_role_members", + self::LOG_DEBUG + ); + $auth_ldap->ldap_close(); + self::debuglog ( + "LDAP closed", + "ldap_get_role_members", + self::LOG_DEBUG + ); + + // Generate SELECT statement for DB call + foreach ($members as $member) { + if (isset($select)) { + $select = "{$select},'{$member}'"; + } else { + $select = "'{$member}'"; + } + } + + // Fetch users from DB to match IDs to usernames + if (isset($select)) { + $select = "username IN ({$select})"; + $members = $DB -> get_recordset_select('user', $select, null, null, 'id,username'); + foreach ($members as $member) { + $return_var[$member -> id] = $member -> username; + } + } else { + self::debuglog ( + "No users found", + "ldap_get_role_members" + ); + } + + return $return_var; + } + + /** + * Returns only the CN of a DN + * + * @param string $dn DN to grab the CN from + * + * @return string CN of the DN + */ + private function get_cn_from_dn ($dn) { + preg_match ("~^cn=(.*?),~", $dn, $matches); + if (count($matches) == 2) { + return $matches[1]; + } + + return false; + } + + /** + * Generate an LDAP search pattern including all groups + * + * @return string + */ + private function ldap_generate_group_pattern() { + $pattern[] = "(objectClass={$this->config->group_object_class})"; + $pattern = '(|' . implode($pattern) . ')'; + return $pattern; + } + + /** + * Syncs course enrolments for a single user + * + * @param string $userid + */ + public function sync_course_enrolments_user ($userid) { + global $DB; + if (!$userid) return; + + self::debuglog ( + "Syncing course enrolments for {$userid}", + "sync_course_enrolments_user", + self::LOG_DEBUG + ); + + $user = \core_user::get_user_by_username ($userid, 'id'); + + if ($this -> is_teacher ($userid)) { + $role = $this -> config -> courses_teacher_role; + } else { + $role = $this -> config -> courses_student_role; + } + + $ldap_courses = $this -> get_courses_ldap ($userid); + $moodle_courses = $this -> get_courses_moodle ($userid); + + $to_enrol = array_diff_key ($ldap_courses, $moodle_courses); + $to_unenrol = array_diff_key ($moodle_courses, $ldap_courses); + + $all_moodle_courses = $this -> get_courses_moodle (); + + self::debuglog ( + "Enrolling user {$userid} to following courses: " . implode (", ", $to_enrol), + "sync_course_enrolments_user", + self::LOG_DEBUG + ); + foreach ($to_enrol as $name => $course) { + $courseObj = $all_moodle_courses[$name]; + + $enrol_instance = $this -> get_enrol_instance ($courseObj); + $this -> enrol_user ($enrol_instance, $user -> id, $role); + } + + self::debuglog ( + "Unenrolling user {$userid} from following courses: " . implode (", ", array_keys($to_unenrol)), + "sync_course_enrolments_user", + self::LOG_DEBUG + ); + foreach ($to_unenrol as $course) { + $enrol_instance = $this -> get_enrol_instance ($course); + $this -> unenrol_user ($enrol_instance, $user -> id); + } + } + + /** + * Ensures proper user enrolment for all IServ-originating courses + */ + public function sync_course_enrolments () { + $courses_category = self::get_courses_category ($this->config); + if (!$courses_category) { + return; + } + + $moodle_courses = $this -> get_courses_moodle(); + + foreach ($moodle_courses as $name => $course) { + $ldap_members = $this -> ldap_get_group_members($name, true); + + $context = context_course::instance ($course -> id); + $enrol_instance = $this -> get_enrol_instance ($course); + + if (!$enrol_instance) { + self::debuglog ( + "Could not get enrol instance for course {$name} @{$course -> id}, ignoring...", + "sync_course_enrolments" + ); + continue; + } + + $moodle_members = self::get_enrolled_usernames ($context); + $this -> course_enrolunenrol ($course, $enrol_instance, $moodle_members, $ldap_members); + } + } + + /** + * Get array of in-course enrolled users + * + * @param context $context Course context + * @return string[] Array of enrolled usernames + */ + private static function get_enrolled_usernames ($context) { + $moodle_user_objects = get_enrolled_users ($context); + $enrolled = array(); + + foreach($moodle_user_objects as $user) { + $enrolled[] = $user->username; + } + + return $enrolled; + } + + /** + * Gets the enrol instance for a course + * + * @param course $course Course to the get enrol instance for + * + * @return stdClass Enrol instance + */ + private function get_enrol_instance($course) { + global $DB; + + $enrol_instance = $DB -> get_record ( + 'enrol', + array( + 'enrol' => $this -> get_name(), + 'courseid' => $course -> id + ) + ); + + if (!$enrol_instance) { + $instanceid = $this -> add_default_instance ($course); + + if ($instanceid === null) { + $instanceid = $this->add_instance($course); + } + + $enrol_instance = $DB -> get_record ( + 'enrol', + array( + 'id' => $instanceid + ) + ); + } + + return $enrol_instance; + } + + /** + * Enrolls and unenrolls users from a course depending on a diff between two username arrays + * + * @param object $course Course object + * @param stdClass $enrol_instance Enrol instance to use for enrolling and Unenrolling + * @param array $current_state Array with currently enrolled users + * @param array $new_state Array with users that shall be enrolled afterwards + */ + private function course_enrolunenrol ($course, $enrol_instance, $current_state, $new_state) { + $to_enrol = array_diff ($new_state, $current_state); + $to_unenrol = array_diff ($current_state, $new_state); + $to_enrol_teachers = array (); + $to_enrol_students = array (); + + foreach ($to_enrol as $user) { + if ($this -> is_teacher ($user)) { + $to_enrol_teachers[] = $user; + } else { + $to_enrol_students[] = $user; + } + } + + if (!empty($to_enrol) || !empty($to_unenrol)) { + self::debuglog ( + "Users to enrol to {$course->shortname}: " . implode(",", $to_enrol), + "course_enrolunenrol", + self::LOG_DEBUG + ); + self::debuglog ( + "Users to unenrol from {$course->shortname}: " . implode(",", $to_unenrol), + "course_enrolunenrol", + self::LOG_DEBUG + ); + } + + if (!empty ($to_enrol_teachers)) { + $this -> course_enrol ( + $course, + $enrol_instance, + $to_enrol_teachers, + $this -> config -> courses_teacher_role + ); + } + + if (!empty ($to_enrol_students)) { + $this -> course_enrol ( + $course, + $enrol_instance, + $to_enrol_students, + $this -> config -> courses_student_role + ); + } + + if (!empty ($to_unenrol)) { + $this -> course_unenrol ( + $course, + $enrol_instance, + $to_unenrol + ); + } + } + + /** + * Enrolls users to a course with a specified role + * + * @param object $course Course object + * @param stdClass $enrol_instance Enrol instance to use for enrolling + * @param array $users Array of usernames to enrol + * @param int $role ID of role to assign to users + */ + private function course_enrol ($course, $enrol_instance, $users, $role) { + global $DB; + + if (!is_array($users)) $users = array($users); + + foreach ($users as $username) { + $user = $DB -> get_record ( + 'user', + array ( + 'username' => $username, + 'auth' => 'ldap' + ) + ); + + if (!$user) { + self::debuglog ( + "User {$username} not found in LDAP users", + "course_enrol" + ); + continue; + } + + $this -> enrol_user ($enrol_instance, $user->id, $role); + self::debuglog ( + "Enrolled user {$username} @{$user -> id} into course {$course -> shortname} @{$course -> id} with role @{$role}", + "course_enrol", + self::LOG_MTRACE + ); + } + } + + /** + * Unenrolls users from a course + * + * @param object $course Course object + * @param stdClass $enrol_instance Enrol instance to use for Unenrolling + * @param array $users Array of usernames to unenrol + */ + private function course_unenrol ($course, $enrol_instance, $users) { + global $DB; + if (!is_array($users)) $users = array($users); + + foreach ($users as $username) { + $user = $DB->get_record ( + 'user', + array ( + 'username' => $username, + 'auth' => 'ldap' + ) + ); + + if (!$user) { + self::debuglog ( + "User {$username} not found in LDAP users", + "course_enrol" + ); + continue; + } + + $this -> unenrol_user ($enrol_instance, $user->id); + + self::debuglog ( + "Unenrolled user {$username} @{$user -> id} from course {$course -> shortname} @{$course -> id}", + "course_unenrol", + self::LOG_MTRACE + ); + } + } +} \ No newline at end of file diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..b600ae3 --- /dev/null +++ b/settings.php @@ -0,0 +1,342 @@ +. + +/** + * IServ enrolment plugin implementation + * + * This plugin synchronizes courses and their enrolments with an IServ school server. + * Based partially on the OSS plugin by Frank Schütte + * + * @package enrol + * @subpackage iserv + * @author Jonas Lührig based on code by Frank Schütte based on code by Iñaki Arenaza + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @copyright 2010 Iñaki Arenaza + * @copyright 2020 Frank Schütte + * @copyright 2023 Gruelag GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($ADMIN -> fulltree) { + global $DB; + + // Initializing + require_once ("{$CFG -> dirroot}/enrol/ldap/settingslib.php"); + require_once ("{$CFG -> dirroot}/enrol/oss/settings_callbacks.php"); + $yesno = array(get_string('no'), get_string('yes')); + + // Heading + $settings -> add( + new admin_setting_heading( + 'enrol_iserv_settings', + get_string('pluginname','enrol_iserv'), + get_string('plugin_description', 'enrol_iserv') + ) + ); + + + // --- GROUP Settings --- // + $settings->add( + new admin_setting_heading( + 'enrol_iserv_group_settings', + get_string('group_settings', 'enrol_iserv'), + get_string('group_settings_desc', 'enrol_iserv') + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/group_contexts', + get_string('group_contexts_key', 'enrol_iserv'), + get_string('group_contexts_desc', 'enrol_iserv'), + 'ou=groups,dc=mein-iserv,dc=de' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/group_object_class', + get_string('group_object_class_key', 'enrol_iserv'), + get_string('group_object_class_desc', 'enrol_iserv'), + 'uuidObject' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/group_attribute', + get_string('group_attribute_key', 'enrol_iserv'), + get_string('group_attribute_desc', 'enrol_iserv'), + 'cn' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/group_fullname_attribute', + get_string('group_fullname_attribute_key', 'enrol_iserv'), + get_string('group_fullname_attribute_desc', 'enrol_iserv'), + 'description' + ) + ); + + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/group_member_attribute', + get_string('group_member_attribute_key', 'enrol_iserv'), + get_string('group_member_attribute_desc', 'enrol_iserv'), + "memberUid" + ) + ); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/group_member_attribute_is_dn', + get_string('group_member_attribute_is_dn_key', 'enrol_iserv'), + get_string('group_member_attribute_is_dn_desc', 'enrol_iserv'), + 0, + $yesno + ) + ); + + $settings->add( + new admin_setting_configcheckbox( + 'enrol_iserv/group_search_subtree', + get_string('group_search_subtree_key', 'enrol_iserv'), + get_string('group_search_subtree_desc', 'enrol_iserv'), + 0 + ) + ); + + + // --- ROLE Settings --- // + $settings->add( + new admin_setting_heading( + 'enrol_iserv_role_settings', + get_string('role_settings', 'enrol_iserv'), + get_string('role_settings_desc', 'enrol_iserv') + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/role_contexts', + get_string('role_contexts_key', 'enrol_iserv'), + get_string('role_contexts_desc', 'enrol_iserv'), + 'ou=roles,dc=mein-iserv,dc=de' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/role_object_class', + get_string('role_object_class_key', 'enrol_iserv'), + get_string('role_object_class_desc', 'enrol_iserv'), + 'organizationalRole' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/teachers_role_name', + get_string('teachers_role_name_key', 'enrol_iserv'), + get_string('teachers_role_name_desc', 'enrol_iserv'), + 'ROLE_TEACHER' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/students_role_name', + get_string('students_role_name_key', 'enrol_iserv'), + get_string('students_role_name_desc', 'enrol_iserv'), + 'ROLE_STUDENT' + ) + ); + + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/role_member_attribute', + get_string('role_member_attribute_key', 'enrol_iserv'), + get_string('role_member_attribute_desc', 'enrol_iserv'), + "roleOccupant" + ) + ); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/role_member_attribute_is_dn', + get_string('role_member_attribute_is_dn_key', 'enrol_iserv'), + get_string('role_member_attribute_is_dn_desc', 'enrol_iserv'), + 1, + $yesno + ) + ); + + + // --- GROUP<>COURSE MAPPING Settings --- // + $settings->add( + new admin_setting_heading( + 'enrol_iserv_coursemapping_settings', + get_string('coursemapping', 'enrol_iserv'), + get_string('coursemapping_desc', 'enrol_iserv') + ) + ); + + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/coursemapping_attribute', + get_string('coursemapping_attribute_key', 'enrol_iserv'), + get_string('coursemapping_attribute_desc', 'enrol_iserv'), + 'memberUid' + ) + ); + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/coursemapping_attribute_value', + get_string('coursemapping_attribute_value_key', 'enrol_iserv'), + get_string('coursemapping_attribute_value_desc', 'enrol_iserv'), + "moodlekurs" + ) + ); + + $settings->add( + new admin_setting_configcheckbox( + 'enrol_iserv/coursemapping_use_attribute', + get_string('coursemapping_use_attribute_key', 'enrol_iserv'), + get_string('coursemapping_use_attribute_desc', 'enrol_iserv'), + 0 + ) + ); + + + $settings->add( + new admin_setting_configtext_trim_lower( + 'enrol_iserv/coursemapping_prefixes', + get_string('coursemapping_prefixes_key', 'enrol_iserv'), + get_string('coursemapping_prefixes_desc', 'enrol_iserv'), + "kurs.*" + ) + ); + + $settings->add( + new admin_setting_configcheckbox( + 'enrol_iserv/coursemapping_use_prefixes', + get_string('coursemapping_use_prefixes_key', 'enrol_iserv'), + get_string('coursemapping_use_prefixes_desc', 'enrol_iserv'), + 1 + ) + ); + + + // --- COURSES Settings --- // + $settings->add( + new admin_setting_heading( + 'enrol_iserv_courses_settings', + get_string('courses', 'enrol_iserv'), + get_string('courses_desc', 'enrol_iserv') + ) + ); + + $setting = new admin_setting_configtext_trim_lower( + 'enrol_iserv/courses_category', + get_string('courses_category_key', 'enrol_iserv'), + get_string('courses_category_desc', 'enrol_iserv'), + "Kurse" + ); + $setting -> set_updatedcallback ('enrol_iserv_settings_courses_category_name_updated'); + $settings->add($setting); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/courses_autocreate', + get_string('courses_autocreate_key', 'enrol_iserv'), + get_string('courses_autocreate_desc', 'enrol_iserv'), + 1, + $yesno + ) + ); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/courses_autoremove', + get_string('courses_autoremove_key', 'enrol_iserv'), + get_string('courses_autoremove_desc', 'enrol_iserv'), + 1, + $yesno + ) + ); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/courses_category_autocreate', + get_string('courses_category_autocreate_key', 'enrol_iserv'), + get_string('courses_category_autocreate_desc', 'enrol_iserv'), + 1, + $yesno + ) + ); + + $enrol_iserv_courses = array(); + $enrol_iserv_coursenames = $DB -> get_records_sql('SELECT * FROM {course} ORDER BY fullname'); + foreach ($enrol_iserv_coursenames as $key => $coursename) { + $enrol_iserv_courses[$coursename->id] = "{$coursename -> fullname} ({$coursename->id})"; + } + $enrol_iserv_courses[0] = get_string('course_template_none','enrol_iserv'); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/courses_template', + get_string('courses_template_key', 'enrol_iserv'), + get_string('courses_template_desc', 'enrol_iserv'), + 0, + $enrol_iserv_courses + ) + ); + + $options = get_default_enrol_roles (context_system::instance ()); + $teacher = get_archetype_roles ('editingteacher'); + $teacher = reset ($teacher); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/courses_teacher_role', + get_string('courses_teacher_role_key', 'enrol_iserv'), + get_string('courses_teacher_role_desc', 'enrol_iserv'), + $teacher -> id, + $options + ) + ); + + $options = get_default_enrol_roles (context_system::instance ()); + $student = get_archetype_roles ('student'); + $student = reset ($student); + + $settings->add( + new admin_setting_configselect( + 'enrol_iserv/courses_student_role', + get_string('courses_student_role_key', 'enrol_iserv'), + get_string('courses_student_role_desc', 'enrol_iserv'), + $student -> id, + $options + ) + ); +} diff --git a/settings_callbacks.php b/settings_callbacks.php new file mode 100644 index 0000000..7c80115 --- /dev/null +++ b/settings_callbacks.php @@ -0,0 +1,90 @@ +lear. + +/** + * IServ enrolment plugin implementation + * + * This plugin synchronizes courses and their enrolments with an IServ school server. + * Based partially on the OSS plugin by Frank Schütte + * + * @package enrol + * @subpackage iserv + * @author Jonas Lührig based on code by Frank Schütte based on code by Iñaki Arenaza + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @copyright 2010 Iñaki Arenaza + * @copyright 2020 Frank Schütte + * @copyright 2023 Gruelag GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Callback for updating the courses category name on settings update + * + * @uses $CFG + */ +function enrol_iserv_settings_courses_category_name_updated ($value = "") { + global $CFG; + + file_put_contents ("/tmp/test.txt", "here {$value}"); + + require_once "{$CFG -> dirroot}/enrol/iserv/lib.php"; + + $enrol = enrol_get_plugin('iserv'); + $enrol -> rename_courses_category ( + get_config ('enrol_iserv') -> courses_category + ); +} + +/* +function enrol_oss_settings_class_category_updated($full_name) { + global $CFG; + + require_once $CFG->dirroot.'/enrol/oss/lib.php'; + + $config = get_config('enrol_oss'); + $classcat = enrol_oss_plugin::get_class_category($config); + if ( $classcat ) { + $classcat->update(array('name' => $config->class_category)); + } +} + +function enrol_oss_description_updated($groupname, $newvalue) { + global $DB; + + $data = new stdClass; + $records = $DB->get_records_sql("SELECT g.id, g.courseid, c.shortname FROM {groups} g JOIN {course} c ON g.courseid = c.id WHERE g.name = ? ", array($groupname)); + foreach($records as $group) { + $data->id = $group->id; + $data->description = '

'.trim($newvalue).' '.$group->shortname.'

'; + $DB->update_record('groups', $data); + } +} + +function enrol_oss_settings_class_teachers_group_description_updated($full_name) { + $newvalue = get_config('enrol_oss', 'class_teachers_group_description'); + enrol_oss_description_updated('teachers', $newvalue); +} + +function enrol_oss_settings_class_students_group_description_updated($full_name) { + $newvalue = get_config('enrol_oss', 'class_students_group_description'); + enrol_oss_description_updated('students', $newvalue); +} + +function enrol_oss_settings_class_parents_group_description_updated($full_name) { + $newvalue = get_config('enrol_oss', 'class_parents_group_description'); + enrol_oss_description_updated('parents', $newvalue); +} +*/ \ No newline at end of file diff --git a/version.php b/version.php new file mode 100644 index 0000000..b32395b --- /dev/null +++ b/version.php @@ -0,0 +1,40 @@ +. + +/** + * IServ enrolment plugin implementation + * + * This plugin synchronizes courses and their enrolments with an IServ school server. + * Based partially on the OSS plugin by Frank Schütte + * + * @package enrol + * @subpackage iserv + * @author Jonas Lührig based on code by Frank Schütte based on code by Iñaki Arenaza + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @copyright 2010 Iñaki Arenaza + * @copyright 2020 Frank Schütte + * @copyright 2023 Gruelag GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin -> version = 2023102210; // The current plugin version (Date: YYYYMMDDXX). +$plugin -> requires = 2015051100; // Requires Moodle version 2.9 +$plugin -> component = 'enrol_iserv'; // Full name of the plugin (used for diagnostics). +$plugin -> maturity = MATURITY_BETA; // Beta, nees testing. +$plugin -> release = '2.3 (Build: 2023081202)'; +$plugin -> dependencies = array('auth_ldap' => ANY_VERSION);