You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1344 lines
44 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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 <iarenaza@eps.mondragon.edu>
* @copyright 2020 Frank Schütte <fschuett@gymhim.de>
* @copyright 2023 Gruelag GmbH <buero@gruelag.de>
* @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
);
}
}
}