. /** * 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 ); } } }