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
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
|
|
);
|
|
}
|
|
}
|
|
} |