/enrol/ldap/lib.php
PHP | 992 lines | 649 code | 114 blank | 229 comment | 128 complexity | c8f44b41579f1ce341363caefefd5a88 MD5 | raw file
- <?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
- * @subpackage 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] ';
- /**
- * 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();
- // 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];
- }
- }
- if (empty($this->config->objectclass)) {
- // Can't send empty filter. Fix it for now and future occasions
- $this->set_config('objectclass', '(objectClass=*)');
- } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
- // Value is 'objectClass=some-string-here', so just add ()
- // around the value (filter _must_ have them).
- // Fix it for now and future occasions
- $this->set_config('objectclass', '('.$this->config->objectclass.')');
- } else if (stripos($this->config->objectclass, '(') !== 0) {
- // Value is 'some-string-not-starting-with-left-parentheses',
- // which is assumed to be the objectClass matching value.
- // So build a valid filter with it.
- $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')');
- } else {
- // There is an additional possible value
- // '(some-string-here)', that can be used to specify any
- // valid filter string, to select subsets of users based
- // on any criteria. For example, we could select the users
- // whose objectClass is 'user' and have the
- // 'enabledMoodleUser' attribute, with something like:
- //
- // (&(objectClass=user)(enabledMoodleUser=1))
- //
- // In this particular case we don't need to do anything,
- // so leave $this->config->objectclass as is.
- }
- }
- /**
- * Is it possible to delete enrol instance via standard UI?
- *
- * @param object $instance
- * @return bool
- */
- public function instance_deleteable($instance) {
- 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 course
- return false;
- }
- /**
- * 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.
- $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 here
- @set_time_limit(0);
- 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.shortname
- FROM {user} u
- JOIN {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 exist
- if ($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 enrolid
- FROM {course} c
- JOIN {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 enrolments
- break;
- 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'));
- // we may need a lot of memory here
- @set_time_limit(0);
- 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(textlib::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 = '';
- foreach ($ldap_contexts as $ldap_context) {
- $ldap_context = trim($ldap_context);
- if (empty($ldap_context)) {
- continue; // Next;
- }
- $flat_records = array();
- do {
- if ($ldap_pagedresults) {
- ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $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);
- } else {
- // Search only in this context
- $ldap_result = @ldap_list($this->ldapconnection,
- $ldap_context,
- $ldap_search_pattern,
- $ldap_fields_wanted);
- }
- if (!$ldap_result) {
- continue; // Next
- }
- if ($ldap_pagedresults) {
- ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_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 mem
- unset($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 exist
- if ($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!
- }
- }
- // Enrol & unenrol
- // Pull the ldap membership into a nice array
- // this is an odd array -- mix of hash and array --
- $ldapmembers = array();
- if (array_key_exists('memberattribute_role'.$role->id, $this->config)
- && !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, '(objectClass=*)',
- 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 instanceid
- FROM {user} u
- JOIN {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 enrolments
- break;
- 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 enrolid
- FROM {course} c
- JOIN {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.status
- FROM {user_enrolments} ue
- JOIN {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 enrolments
- return array();
- }
- $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
- if (empty($ldap_contexts)) {
- // No role contexts, so no LDAP enrolments
- return array();
- }
- $extmemberuid = textlib::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) {
- $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 pattern
- if (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'));
- foreach ($ldap_contexts as $context) {
- $context = trim($context);
- if (empty($context)) {
- continue;
- }
- $ldap_cookie = '';
- $flat_records = array();
- do {
- if ($ldap_pagedresults) {
- ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $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);
- } else {
- // Search only in this context
- $ldap_result = @ldap_list($this->ldapconnection,
- $context,
- $ldap_search_pattern,
- $ldap_fields_wanted);
- }
- if (!$ldap_result) {
- continue;
- }
- if ($ldap_pagedresults) {
- ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_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 mem
- unset($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'));
- $ldap_defaults = ldap_getdefaults();
- return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
- '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
- $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 count
- continue;
- }
- 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 warning
- if ($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 record
- unset($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->groupmodeforce = $courseconfig->groupmodeforce;
- }
- $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 {
-