Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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/>./*** LDAP enrolment plugin implementation.** This plugin synchronises enrolment and roles with a LDAP server.** @package enrol_ldap* @author Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}* @copyright 2010 Iñaki Arenaza <iarenaza@eps.mondragon.edu>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();class enrol_ldap_plugin extends enrol_plugin {protected $enrol_localcoursefield = 'idnumber';protected $enroltype = 'enrol_ldap';protected $errorlogtag = '[ENROL LDAP] ';/*** The object class to use when finding users.** @var string $userobjectclass*/protected $userobjectclass;/** @var LDAP\Connection LDAP connection. */protected $ldapconnection;/*** Constructor for the plugin. In addition to calling the parent* constructor, we define and 'fix' some settings depending on the* real settings the admin defined.*/public function __construct() {global $CFG;require_once($CFG->libdir.'/ldaplib.php');// Do our own stuff to fix the config (it's easier to do it// here than using the admin settings infrastructure). We// don't call $this->set_config() for any of the 'fixups'// (except the objectclass, as it's critical) because the user// didn't specify any values and relied on the default values// defined for the user type she chose.$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();// The objectclass in the defaults is for a user.// This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass.// Save the normalised user objectclass for later.$this->userobjectclass = ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]);// Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.unset($default['objectclass']);// 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];}}// Normalise the objectclass used for groups.if (empty($this->config->objectclass)) {// No objectclass set yet - set a default class.$this->config->objectclass = ldap_normalise_objectclass(null, '*');$this->set_config('objectclass', $this->config->objectclass);} else {$objectclass = ldap_normalise_objectclass($this->config->objectclass);if ($objectclass !== $this->config->objectclass) {// The objectclass was changed during normalisation.// Save it in config, and update the local copy of config.$this->set_config('objectclass', $objectclass);$this->config->objectclass = $objectclass;}}}/*** Is it possible to delete enrol instance via standard UI?** @param object $instance* @return bool*/public function can_delete_instance($instance) {$context = context_course::instance($instance->courseid);if (!has_capability('enrol/ldap:manage', $context)) {return false;}if (!enrol_is_enabled('ldap')) {return true;}if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {return true;}// TODO: connect to external system and make sure no users are to be enrolled in this coursereturn false;}/*** Is it possible to hide/show enrol instance via standard UI?** @param stdClass $instance* @return bool*/public function can_hide_show_instance($instance) {$context = context_course::instance($instance->courseid);return has_capability('enrol/ldap:manage', $context);}/*** Forces synchronisation of user enrolments with LDAP server.* It creates courses if the plugin is configured to do so.** @param object $user user record* @return void*/public function sync_user_enrolments($user) {global $DB;// Do not try to print anything to the output because this method is called during interactive login.if (PHPUNIT_TEST) {$trace = new null_progress_trace();} else {$trace = new error_log_progress_trace($this->errorlogtag);}if (!$this->ldap_connect($trace)) {$trace->finished();return;}if (!is_object($user) or !property_exists($user, 'id')) {throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');}if (!property_exists($user, 'idnumber')) {debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');$user = $DB->get_record('user', array('id'=>$user->id));}// We may need a lot of memory herecore_php_time_limit::raise();raise_memory_limit(MEMORY_HUGE);// Get enrolments for each type of role.$roles = get_all_roles();$enrolments = array();foreach($roles as $role) {// Get external enrolments according to LDAP server$enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role);// Get the list of current user enrolments that come from LDAP$sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortnameFROM {user} uJOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)JOIN {enrol} e ON (e.id = ue.enrolid)JOIN {course} c ON (c.id = e.courseid)WHERE u.deleted = 0 AND u.id = :userid";$params = array ('roleid'=>$role->id, 'userid'=>$user->id);$enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params);}$ignorehidden = $this->get_config('ignorehiddencourses');$courseidnumber = $this->get_config('course_idnumber');foreach($roles as $role) {foreach ($enrolments[$role->id]['ext'] as $enrol) {$course_ext_id = $enrol[$courseidnumber][0];if (empty($course_ext_id)) {$trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));continue; // Next; skip this one!}// Create the course if required$course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));if (empty($course)) { // Course doesn't existif ($this->get_config('autocreate')) { // Autocreate$trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));if (!$newcourseid = $this->create_course($enrol, $trace)) {continue;}$course = $DB->get_record('course', array('id'=>$newcourseid));} else {$trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));continue; // Next; skip this one!}}// Deal with enrolment in the moodle db// Add necessary enrol instance if not present yet;$sql = "SELECT c.id, c.visible, e.id as enrolidFROM {course} cJOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')WHERE c.id = :courseid";$params = array('courseid'=>$course->id);if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {$course_instance = new stdClass();$course_instance->id = $course->id;$course_instance->visible = $course->visible;$course_instance->enrolid = $this->add_instance($course_instance);}if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {continue; // Weird; skip this one.}if ($ignorehidden && !$course_instance->visible) {continue;}if (empty($enrolments[$role->id]['current'][$course->id])) {// Enrol the user in the given course, with that role.$this->enrol_user($instance, $user->id, $role->id);// Make sure we set the enrolment status to active. If the user wasn't// previously enrolled to the course, enrol_user() sets it. But if we// configured the plugin to suspend the user enrolments _AND_ remove// the role assignments on external unenrol, then enrol_user() doesn't// set it back to active on external re-enrolment. So set it// unconditionnally to cover both cases.$DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));$trace->output(get_string('enroluser', 'enrol_ldap',array('user_username'=> $user->username,'course_shortname'=>$course->shortname,'course_id'=>$course->id)));} else {if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {// Reenable enrolment that was previously disabled. Enrolment refreshed$DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));$trace->output(get_string('enroluserenable', 'enrol_ldap',array('user_username'=> $user->username,'course_shortname'=>$course->shortname,'course_id'=>$course->id)));}}// Remove this course from the current courses, to be able to detect// which current courses should be unenroled from when we finish processing// external enrolments.unset($enrolments[$role->id]['current'][$course->id]);}// Deal with unenrolments.$transaction = $DB->start_delegated_transaction();foreach ($enrolments[$role->id]['current'] as $course) {$context = context_course::instance($course->courseid);$instance = $DB->get_record('enrol', array('id'=>$course->enrolid));switch ($this->get_config('unenrolaction')) {case ENROL_EXT_REMOVED_UNENROL:$this->unenrol_user($instance, $user->id);$trace->output(get_string('extremovedunenrol', 'enrol_ldap',array('user_username'=> $user->username,'course_shortname'=>$course->shortname,'course_id'=>$course->courseid)));break;case ENROL_EXT_REMOVED_KEEP:// Keep - only adding enrolmentsbreak;case ENROL_EXT_REMOVED_SUSPEND:if ($course->status != ENROL_USER_SUSPENDED) {$DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));$trace->output(get_string('extremovedsuspend', 'enrol_ldap',array('user_username'=> $user->username,'course_shortname'=>$course->shortname,'course_id'=>$course->courseid)));}break;case ENROL_EXT_REMOVED_SUSPENDNOROLES:if ($course->status != ENROL_USER_SUSPENDED) {$DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));}role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));$trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',array('user_username'=> $user->username,'course_shortname'=>$course->shortname,'course_id'=>$course->courseid)));break;}}$transaction->allow_commit();}$this->ldap_close();$trace->finished();}/*** Forces synchronisation of all enrolments with LDAP server.* It creates courses if the plugin is configured to do so.** @param progress_trace $trace* @param int|null $onecourse limit sync to one course->id, null if all courses* @return void*/public function sync_enrolments(progress_trace $trace, $onecourse = null) {global $CFG, $DB;if (!$this->ldap_connect($trace)) {$trace->finished();return;}$ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);// we may need a lot of memory herecore_php_time_limit::raise();raise_memory_limit(MEMORY_HUGE);$oneidnumber = null;if ($onecourse) {if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) {// Course does not exist, nothing to do.$trace->output("Requested course $onecourse does not exist, no sync performed.");$trace->finished();return;}if (empty($course->{$this->enrol_localcoursefield})) {$trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");$trace->finished();return;}$oneidnumber = ldap_filter_addslashes(core_text::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding')));}// Get enrolments for each type of role.$roles = get_all_roles();$enrolments = array();foreach($roles as $role) {// Get all contexts$ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});// Get all the fields we will want for the potential course creation// as they are light. Don't get membership -- potentially a lot of data.$ldap_fields_wanted = array('dn', $this->config->course_idnumber);if (!empty($this->config->course_fullname)) {array_push($ldap_fields_wanted, $this->config->course_fullname);}if (!empty($this->config->course_shortname)) {array_push($ldap_fields_wanted, $this->config->course_shortname);}if (!empty($this->config->course_summary)) {array_push($ldap_fields_wanted, $this->config->course_summary);}array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});// Define the search pattern$ldap_search_pattern = $this->config->objectclass;if ($oneidnumber !== null) {$ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";}$ldap_cookie = '';$servercontrols = array();foreach ($ldap_contexts as $ldap_context) {$ldap_context = trim($ldap_context);if (empty($ldap_context)) {continue; // Next;}$flat_records = array();do {if ($ldap_pagedresults) {$servercontrols = array(array('oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array('size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));}if ($this->config->course_search_sub) {// Use ldap_search to find first user from subtree$ldap_result = @ldap_search($this->ldapconnection, $ldap_context,$ldap_search_pattern, $ldap_fields_wanted,0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);} else {// Search only in this context$ldap_result = @ldap_list($this->ldapconnection, $ldap_context,$ldap_search_pattern, $ldap_fields_wanted,0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);}if (!$ldap_result) {continue; // Next}if ($ldap_pagedresults) {// Get next server cookie to know if we'll need to continue searching.$ldap_cookie = '';// Get next cookie from controls.ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,$errmsg, $referrals, $controls);if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {$ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];}}// Check and push results$records = ldap_get_entries($this->ldapconnection, $ldap_result);// LDAP libraries return an odd array, really. fix it:for ($c = 0; $c < $records['count']; $c++) {array_push($flat_records, $records[$c]);}// Free some memunset($records);} while ($ldap_pagedresults && !empty($ldap_cookie));// If LDAP paged results were used, the current connection must be completely// closed and a new one created, to work without paged results from here on.if ($ldap_pagedresults) {$this->ldap_close();$this->ldap_connect($trace);}if (count($flat_records)) {$ignorehidden = $this->get_config('ignorehiddencourses');foreach($flat_records as $course) {$course = array_change_key_case($course, CASE_LOWER);$idnumber = $course[$this->config->course_idnumber][0];$trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));// Does the course exist in moodle already?$course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));if (empty($course_obj)) { // Course doesn't existif ($this->get_config('autocreate')) { // Autocreate$trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));if (!$newcourseid = $this->create_course($course, $trace)) {continue;}$course_obj = $DB->get_record('course', array('id'=>$newcourseid));} else {$trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));continue; // Next; skip this one!}} else { // Check if course needs update & update as needed.$this->update_course($course_obj, $course, $trace);}// Enrol & unenrol// Pull the ldap membership into a nice array// this is an odd array -- mix of hash and array --$ldapmembers = array();if (property_exists($this->config, 'memberattribute_role'.$role->id)&& !empty($this->config->{'memberattribute_role'.$role->id})&& !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!$ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];unset($ldapmembers['count']); // Remove oddity ;)// If we have enabled nested groups, we need to expand// the groups to get the real user list. We need to do// this before dealing with 'memberattribute_isdn'.if ($this->config->nested_groups) {$users = array();foreach ($ldapmembers as $ldapmember) {$grpusers = $this->ldap_explode_group($ldapmember,$this->config->{'memberattribute_role'.$role->id});$users = array_merge($users, $grpusers);}$ldapmembers = array_unique($users); // There might be duplicates.}// Deal with the case where the member attribute holds distinguished names,// but only if the user attribute is not a distinguished name itself.if ($this->config->memberattribute_isdn&& ($this->config->idnumber_attribute !== 'dn')&& ($this->config->idnumber_attribute !== 'distinguishedname')) {// We need to retrieve the idnumber for all the users in $ldapmembers,// as the idnumber does not match their dn and we get dn's from membership.$memberidnumbers = array();foreach ($ldapmembers as $ldapmember) {$result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,array($this->config->idnumber_attribute));$entry = ldap_first_entry($this->ldapconnection, $result);$values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);array_push($memberidnumbers, $values[0]);}$ldapmembers = $memberidnumbers;}}// Prune old ldap enrolments// hopefully they'll fit in the max buffer size for the RDBMS$sql= "SELECT u.id as userid, u.username, ue.status,ra.contextid, ra.itemid as instanceidFROM {user} uJOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)JOIN {enrol} e ON (e.id = ue.enrolid)WHERE u.deleted = 0 AND e.courseid = :courseid ";$params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);$context = context_course::instance($course_obj->id);if (!empty($ldapmembers)) {list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);$sql .= "AND u.idnumber $ldapml";$params = array_merge($params, $params2);unset($params2);} else {$shortname = format_string($course_obj->shortname, true, array('context' => $context));$trace->output(get_string('emptyenrolment', 'enrol_ldap',array('role_shortname'=> $role->shortname,'course_shortname' => $shortname)));}$todelete = $DB->get_records_sql($sql, $params);if (!empty($todelete)) {$transaction = $DB->start_delegated_transaction();foreach ($todelete as $row) {$instance = $DB->get_record('enrol', array('id'=>$row->instanceid));switch ($this->get_config('unenrolaction')) {case ENROL_EXT_REMOVED_UNENROL:$this->unenrol_user($instance, $row->userid);$trace->output(get_string('extremovedunenrol', 'enrol_ldap',array('user_username'=> $row->username,'course_shortname'=>$course_obj->shortname,'course_id'=>$course_obj->id)));break;case ENROL_EXT_REMOVED_KEEP:// Keep - only adding enrolmentsbreak;case ENROL_EXT_REMOVED_SUSPEND:if ($row->status != ENROL_USER_SUSPENDED) {$DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));$trace->output(get_string('extremovedsuspend', 'enrol_ldap',array('user_username'=> $row->username,'course_shortname'=>$course_obj->shortname,'course_id'=>$course_obj->id)));}break;case ENROL_EXT_REMOVED_SUSPENDNOROLES:if ($row->status != ENROL_USER_SUSPENDED) {$DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));}role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));$trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',array('user_username'=> $row->username,'course_shortname'=>$course_obj->shortname,'course_id'=>$course_obj->id)));break;}}$transaction->allow_commit();}// Insert current enrolments// bad we can't do INSERT IGNORE with postgres...// Add necessary enrol instance if not present yet;$sql = "SELECT c.id, c.visible, e.id as enrolidFROM {course} cJOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')WHERE c.id = :courseid";$params = array('courseid'=>$course_obj->id);if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {$course_instance = new stdClass();$course_instance->id = $course_obj->id;$course_instance->visible = $course_obj->visible;$course_instance->enrolid = $this->add_instance($course_instance);}if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {continue; // Weird; skip this one.}if ($ignorehidden && !$course_instance->visible) {continue;}$transaction = $DB->start_delegated_transaction();foreach ($ldapmembers as $ldapmember) {$sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';$member = $DB->get_record_sql($sql, array($ldapmember));if(empty($member) || empty($member->id)){$trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));continue;}$sql= "SELECT ue.statusFROM {user_enrolments} ueJOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')WHERE e.courseid = :courseid AND ue.userid = :userid";$params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);$userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);if (empty($userenrolment)) {$this->enrol_user($instance, $member->id, $role->id);// Make sure we set the enrolment status to active. If the user wasn't// previously enrolled to the course, enrol_user() sets it. But if we// configured the plugin to suspend the user enrolments _AND_ remove// the role assignments on external unenrol, then enrol_user() doesn't// set it back to active on external re-enrolment. So set it// unconditionally to cover both cases.$DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));$trace->output(get_string('enroluser', 'enrol_ldap',array('user_username'=> $member->username,'course_shortname'=>$course_obj->shortname,'course_id'=>$course_obj->id)));} else {if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) {// This happens when reviving users or when user has multiple roles in one course.$context = context_course::instance($course_obj->id);role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id);$trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");}if ($userenrolment->status == ENROL_USER_SUSPENDED) {// Reenable enrolment that was previously disabled. Enrolment refreshed$DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));$trace->output(get_string('enroluserenable', 'enrol_ldap',array('user_username'=> $member->username,'course_shortname'=>$course_obj->shortname,'course_id'=>$course_obj->id)));}}}$transaction->allow_commit();}}}}@$this->ldap_close();$trace->finished();}/*** Connect to the LDAP server, using the plugin configured* settings. It's actually a wrapper around ldap_connect_moodle()** @param progress_trace $trace* @return bool success*/protected function ldap_connect(progress_trace $trace = null) {global $CFG;require_once($CFG->libdir.'/ldaplib.php');if (isset($this->ldapconnection)) {return true;}if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),$this->get_config('user_type'), $this->get_config('bind_dn'),$this->get_config('bind_pw'), $this->get_config('opt_deref'),$debuginfo, $this->get_config('start_tls'))) {$this->ldapconnection = $ldapconnection;return true;}if ($trace) {$trace->output($debuginfo);} else {error_log($this->errorlogtag.$debuginfo);}return false;}/*** Disconnects from a LDAP server**/protected function ldap_close() {if (isset($this->ldapconnection)) {@ldap_close($this->ldapconnection);$this->ldapconnection = null;}return;}/*** Return multidimensional array with details of user courses (at* least dn and idnumber).** @param string $memberuid user idnumber (without magic quotes).* @param object role is a record from the mdl_role table.* @return array*/protected function find_ext_enrolments($memberuid, $role) {global $CFG;require_once($CFG->libdir.'/ldaplib.php');if (empty($memberuid)) {// No "idnumber" stored for this user, so no LDAP enrolmentsreturn array();}$ldap_contexts = trim($this->get_config('contexts_role'.$role->id));if (empty($ldap_contexts)) {// No role contexts, so no LDAP enrolmentsreturn array();}$extmemberuid = core_text::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));if($this->get_config('memberattribute_isdn')) {if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {return array();}}$ldap_search_pattern = '';if($this->get_config('nested_groups')) {$usergroups = $this->ldap_find_user_groups($extmemberuid);if(count($usergroups) > 0) {foreach ($usergroups as $group) {$group = ldap_filter_addslashes($group);$ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';}}}// Default return value$courses = array();// Get all the fields we will want for the potential course creation// as they are light. don't get membership -- potentially a lot of data.$ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));$fullname = $this->get_config('course_fullname');$shortname = $this->get_config('course_shortname');$summary = $this->get_config('course_summary');if (isset($fullname)) {array_push($ldap_fields_wanted, $fullname);}if (isset($shortname)) {array_push($ldap_fields_wanted, $shortname);}if (isset($summary)) {array_push($ldap_fields_wanted, $summary);}// Define the search patternif (empty($ldap_search_pattern)) {$ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';} else {$ldap_search_pattern = '(|' . $ldap_search_pattern .'('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .')';}$ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';// Get all contexts and look for first matching user$ldap_contexts = explode(';', $ldap_contexts);$ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);foreach ($ldap_contexts as $context) {$context = trim($context);if (empty($context)) {continue;}$ldap_cookie = '';$servercontrols = array();$flat_records = array();do {if ($ldap_pagedresults) {$servercontrols = array(array('oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array('size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));}if ($this->get_config('course_search_sub')) {// Use ldap_search to find first user from subtree$ldap_result = @ldap_search($this->ldapconnection, $context,$ldap_search_pattern, $ldap_fields_wanted,0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);} else {// Search only in this context$ldap_result = @ldap_list($this->ldapconnection, $context,$ldap_search_pattern, $ldap_fields_wanted,0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);}if (!$ldap_result) {continue;}if ($ldap_pagedresults) {// Get next server cookie to know if we'll need to continue searching.$ldap_cookie = '';// Get next cookie from controls.ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,$errmsg, $referrals, $controls);if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {$ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];}}// Check and push results. ldap_get_entries() already// lowercases the attribute index, so there's no need to// use array_change_key_case() later.$records = ldap_get_entries($this->ldapconnection, $ldap_result);// LDAP libraries return an odd array, really. Fix it.for ($c = 0; $c < $records['count']; $c++) {array_push($flat_records, $records[$c]);}// Free some memunset($records);} while ($ldap_pagedresults && !empty($ldap_cookie));// If LDAP paged results were used, the current connection must be completely// closed and a new one created, to work without paged results from here on.if ($ldap_pagedresults) {$this->ldap_close();$this->ldap_connect();}if (count($flat_records)) {$courses = array_merge($courses, $flat_records);}}return $courses;}/*** Search specified contexts for the specified userid and return the* user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper* around ldap_find_userdn().** @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).* @return mixed the user dn or false*/protected function ldap_find_userdn($userid) {global $CFG;require_once($CFG->libdir.'/ldaplib.php');$ldap_contexts = explode(';', $this->get_config('user_contexts'));return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,$this->userobjectclass,$this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));}/*** Find the groups a given distinguished name belongs to, both directly* and indirectly via nested groups membership.** @param string $memberdn distinguished name to search* @return array with member groups' distinguished names (can be emtpy)*/protected function ldap_find_user_groups($memberdn) {$groups = array();$this->ldap_find_user_groups_recursively($memberdn, $groups);return $groups;}/*** Recursively process the groups the given member distinguished name* belongs to, adding them to the already processed groups array.** @param string $memberdn distinguished name to search* @param array reference &$membergroups array with already found* groups, where we'll put the newly found* groups.*/protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {$result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));if (!$result) {return;}if ($entry = ldap_first_entry($this->ldapconnection, $result)) {do {$attributes = ldap_get_attributes($this->ldapconnection, $entry);for ($j = 0; $j < $attributes['count']; $j++) {$groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);foreach ($groups as $key => $group) {if ($key === 'count') { // Skip the entries countcontinue;}if(!in_array($group, $membergroups)) {// Only push and recurse if we haven't 'seen' this group before// to prevent loops (MS Active Directory allows them!!).array_push($membergroups, $group);$this->ldap_find_user_groups_recursively($group, $membergroups);}}}}while ($entry = ldap_next_entry($this->ldapconnection, $entry));}}/*** Given a group name (either a RDN or a DN), get the list of users* belonging to that group. If the group has nested groups, expand all* the intermediate groups and return the full list of users that* directly or indirectly belong to the group.** @param string $group the group name to search* @param string $memberattibute the attribute that holds the members of the group* @return array the list of users belonging to the group. If $group* is not actually a group, returns array($group).*/protected function ldap_explode_group($group, $memberattribute) {switch ($this->get_config('user_type')) {case 'ad':// $group is already the distinguished name to search.$dn = $group;$result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass'));$entry = ldap_first_entry($this->ldapconnection, $result);$objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass');if (!in_array('group', $objectclass)) {// Not a group, so return immediately.return array($group);}$result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute));$entry = ldap_first_entry($this->ldapconnection, $result);$members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warningif ($members['count'] == 0) {// There are no members in this group, return nothing.return array();}unset($members['count']);$users = array();foreach ($members as $member) {$group_members = $this->ldap_explode_group($member, $memberattribute);$users = array_merge($users, $group_members);}return ($users);break;default:error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',$this->get_config('user_type_name')));return array($group);}}/*** Will create the moodle course from the template* course_ext is an array as obtained from ldap -- flattened somewhat** @param array $course_ext* @param progress_trace $trace* @return mixed false on error, id for the newly created course otherwise.*/function create_course($course_ext, progress_trace $trace) {global $CFG, $DB;require_once("$CFG->dirroot/course/lib.php");// Override defaults with template course$template = false;if ($this->get_config('template')) {if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {$template = fullclone(course_get_format($template)->get_course());unset($template->id); // So we are clear to reinsert the recordunset($template->fullname);unset($template->shortname);unset($template->idnumber);}}if (!$template) {$courseconfig = get_config('moodlecourse');$template = new stdClass();$template->summary = '';$template->summaryformat = FORMAT_HTML;$template->format = $courseconfig->format;$template->newsitems = $courseconfig->newsitems;$template->showgrades = $courseconfig->showgrades;$template->showreports = $courseconfig->showreports;$template->maxbytes = $courseconfig->maxbytes;$template->groupmode = $courseconfig->groupmode;$template->groupmodeforce = $courseconfig->groupmodeforce;$template->visible = $courseconfig->visible;$template->lang = $courseconfig->lang;$template->enablecompletion = $courseconfig->enablecompletion;}$course = $template;$course->category = $this->get_config('category');if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {$categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);$first = reset($categories);$course->category = $first->id;}// Override with required ext data$course->idnumber = $course_ext[$this->get_config('course_idnumber')][0];$course->fullname = $course_ext[$this->get_config('course_fullname')][0];$course->shortname = $course_ext[$this->get_config('course_shortname')][0];if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {// We are in trouble!$trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));return false;}$summary = $this->get_config('course_summary');if (!isset($summary) || empty($course_ext[$summary][0])) {$course->summary = '';} else {$course->summary = $course_ext[$this->get_config('course_summary')][0];}// Check if the shortname already exists if it does - skip course creation.if ($DB->record_exists('course', array('shortname' => $course->shortname))) {$trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));return false;}$newcourse = create_course($course);return $newcourse->id;}/*** Will update a moodle course with new values from LDAP* A field will be updated only if it is marked to be updated* on sync in plugin settings** @param object $course* @param array $externalcourse* @param progress_trace $trace* @return bool*/protected function update_course($course, $externalcourse, progress_trace $trace) {global $CFG, $DB;$coursefields = array ('shortname', 'fullname', 'summary');static $shouldupdate;// Initialize $shouldupdate variable. Set to true if one or more fields are marked for update.if (!isset($shouldupdate)) {$shouldupdate = false;foreach ($coursefields as $field) {$shouldupdate = $shouldupdate || $this->get_config('course_'.$field.'_updateonsync');}}// If we should not update return immediately.if (!$shouldupdate) {return false;}require_once("$CFG->dirroot/course/lib.php");$courseupdated = false;$updatedcourse = new stdClass();$updatedcourse->id = $course->id;// Update course fields if necessary.foreach ($coursefields as $field) {// If field is marked to be updated on sync && field data was changed update it.if ($this->get_config('course_'.$field.'_updateonsync')&& isset($externalcourse[$this->get_config('course_'.$field)][0])&& $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) {$updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0];$courseupdated = true;}}if (!$courseupdated) {$trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));return false;}// Do not allow empty fullname or shortname.if ((isset($updatedcourse->fullname) && empty($updatedcourse->fullname))|| (isset($updatedcourse->shortname) && empty($updatedcourse->shortname))) {// We are in trouble!$trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course));return false;}// Check if the shortname already exists if it does - skip course updating.if (isset($updatedcourse->shortname)&& $DB->record_exists('course', array('shortname' => $updatedcourse->shortname))) {$trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course));return false;}// Finally - update course in DB.update_course($updatedcourse);$trace->output(get_string('courseupdated', 'enrol_ldap', $course));return true;}/*** Automatic enrol sync executed during restore.* Useful for automatic sync by course->idnumber or course category.* @param stdClass $course course record*/public function restore_sync_course($course) {// TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)// NOTE: for now restore does not do any real logging yet, let's do the same here...$trace = new error_log_progress_trace();$this->sync_enrolments($trace, $course->id);}/*** Restore instance and map settings.** @param restore_enrolments_structure_step $step* @param stdClass $data* @param stdClass $course* @param int $oldid*/public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {global $DB;// There is only 1 ldap enrol instance per course.if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) {$instance = reset($instances);$instanceid = $instance->id;} else {$instanceid = $this->add_instance($course, (array)$data);}$step->set_mapping('enrol', $oldid, $instanceid);}/*** Restore user enrolment.** @param restore_enrolments_structure_step $step* @param stdClass $data* @param stdClass $instance* @param int $oldinstancestatus* @param int $userid*/public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {global $DB;if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) {// Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.} else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) {if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {$this->enrol_user($instance, $userid, null, 0, 0, $data->status);}} else {if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {$this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED);}}}/*** Restore role assignment.** @param stdClass $instance* @param int $roleid* @param int $userid* @param int $contextid*/public function restore_role_assignment($instance, $roleid, $userid, $contextid) {global $DB;if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) {// Skip any roles restore, they should be already synced automatically.return;}// Just restore every role.if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id);}}}