. /** * 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 = "*") { // Used by a few conditionals below $ldap_filters = []; // Create course filter by IServ group prefixes if ($this->config->coursemapping_use_prefixes) { $prefixes = explode(',', $this->config->coursemapping_prefixes); foreach ($prefixes as $prefix) { $ldap_filters[] = "({$this->config->group_attribute}={$prefix})"; } } if ($this->config->coursemapping_use_attribute) { // If coursemapping attribute value contains a comma, treat it as list of valid attributes (OR'ed) if (strstr($this->config->coursemapping_attribute_value, '|') !== false) { foreach (explode('|', $this->config->coursemapping_attribute_value) as $prefix) { $ldap_filters[] = "({$this->config->coursemapping_attribute}={$prefix})"; } } else { $ldap_filters[] = "({$this->config->coursemapping_attribute}={$this->config->coursemapping_attribute_value})"; } } $pattern = '(|' . implode($ldap_filters) . ')'; 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); $user_record_filter = []; if ($this->config->coursemapping_map_only_ldap_users) { $user_record_filter['auth'] = 'ldap'; } foreach ($users as $username) { $user = $DB->get_record( 'user', [ ...$user_record_filter, 'username' => $username ] ); 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); $user_record_filter = []; if ($this->config->coursemapping_map_only_ldap_users) { $user_record_filter['auth'] = 'ldap'; } foreach ($users as $username) { $user = $DB->get_record( 'user', [ ...$user_record_filter, 'username' => $username ] ); 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 ); } } }