<?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;

        $ldapconnection = $this->ldap_connect();
        if (!$ldapconnection) {
            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($ldapconnection, $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)) {
                    error_log($this->errorlogtag.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
                        error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
                                                                array('courseextid'=>$course_ext_id)));
                        if ($newcourseid = $this->create_course($enrol)) {
                            $course = $DB->get_record('course', array('id'=>$newcourseid));
                        }
                    } else {
                        error_log($this->errorlogtag.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));
                    error_log($this->errorlogtag.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));
                        error_log($this->errorlogtag.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 = get_context_instance(CONTEXT_COURSE, $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);
                        error_log($this->errorlogtag.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));
                            error_log($this->errorlogtag.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));
                        error_log($this->errorlogtag.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($ldapconnection);
    }

    /**
     * Forces synchronisation of all enrolments with LDAP server.
     * It creates courses if the plugin is configured to do so.
     *
     * @return void
     */
    public function sync_enrolments() {
        global $CFG, $DB;

        $ldapconnection = $this->ldap_connect();
        if (!$ldapconnection) {
            return;
        }

        // 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 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;

            foreach ($ldap_contexts as $ldap_context) {
                $ldap_context = trim($ldap_context);
                if (empty($ldap_context)) {
                    continue; // Next;
                }

                if ($this->config->course_search_sub) {
                    // Use ldap_search to find first user from subtree
                    $ldap_result = @ldap_search($ldapconnection,
                                                $ldap_context,
                                                $ldap_search_pattern,
                                                $ldap_fields_wanted);
                } else {
                    // Search only in this context
                    $ldap_result = @ldap_list($ldapconnection,
                                              $ldap_context,
                                              $ldap_search_pattern,
                                              $ldap_fields_wanted);
                }
                if (!$ldap_result) {
                    continue; // Next
                }

                // Check and push results
                $records = ldap_get_entries($ldapconnection, $ldap_result);

                // LDAP libraries return an odd array, really. fix it:
                $flat_records = array();
                for ($c = 0; $c < $records['count']; $c++) {
                    array_push($flat_records, $records[$c]);
                }
                // Free some mem
                unset($records);

                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];
                        print_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
                                error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
                                                                        array('courseextid'=>$idnumber)));
                                if ($newcourseid = $this->create_course($course)) {
                                    $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
                                }
                            } else {
                                error_log($this->errorlogtag.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($ldapconnection,
                                                                          $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($ldapconnection, $ldapmember, '(objectClass=*)',
                                                        array($this->config->idnumber_attribute));
                                    $entry = ldap_first_entry($ldapconnection, $result);
                                    $values = ldap_get_values($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 = get_context_instance(CONTEXT_COURSE, $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));
                            print_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);
                                    error_log($this->errorlogtag.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));
                                        error_log($this->errorlogtag.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));
                                    error_log($this->errorlogtag.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)){
                                print_string ('couldnotfinduser', 'enrol_ldap', $ldapmember);
                                continue;
                            }

                            $sql= "SELECT ue.status
                                     FROM {user_enrolments} ue
                                     JOIN {enrol} e ON (e.id = ue.enrolid)
                                     JOIN {role_assignments} ra ON (ra.itemid = e.id AND ra.component = '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);

                            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
                                // unconditionnally to cover both cases.
                                $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
                                error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
                                                                        array('user_username'=> $member->username,
                                                                              'course_shortname'=>$course_obj->shortname,
                                                                              'course_id'=>$course_obj->id)));

                            } else {
                                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));
                                    error_log($this->errorlogtag.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();
    }

    /**
     * Connect to the LDAP server, using the plugin configured
     * settings. It's actually a wrapper around ldap_connect_moodle()
     *
     * @return mixed A valid LDAP connection or false.
     */
    protected function ldap_connect() {
        global $CFG;
        require_once($CFG->libdir.'/ldaplib.php');

        // Cache ldap connections. They are expensive to set up
        // and can drain the TCP/IP ressources on the server if we
        // are syncing a lot of users (as we try to open a new connection
        // to get the user details). This is the least invasive way
        // to reuse existing connections without greater code surgery.
        if(!empty($this->ldapconnection)) {
            $this->ldapconns++;
            return $this->ldapconnection;
        }

        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->ldapconns = 1;
            $this->ldapconnection = $ldapconnection;
            return $ldapconnection;
        }

        // Log the problem, but don't show it to the user. She doesn't
        // even have a chance to see it, as we redirect instantly to
        // the user/front page.
        error_log($this->errorlogtag.$debuginfo);

        return false;
    }

    /**
     * Disconnects from a LDAP server
     *
     */
    protected function ldap_close() {
        $this->ldapconns--;
        if($this->ldapconns == 0) {
            @ldap_close($this->ldapconnection);
            unset($this->ldapconnection);
        }
    }

    /**
     * Return multidimensional array with details of user courses (at
     * least dn and idnumber).
     *
     * @param resource $ldapconnection a valid LDAP connection.
     * @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 ($ldapconnection, $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 ($ldapconnection, $extmemberuid))) {
                return array();
            }
        }

        $ldap_search_pattern = '';
        if($this->get_config('nested_groups')) {
            $usergroups = $this->ldap_find_user_groups($ldapconnection, $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);
        foreach ($ldap_contexts as $context) {
            $context = trim($context);
            if (empty($context)) {
                continue;
            }

            if ($this->get_config('course_search_sub')) {
                // Use ldap_search to find first user from subtree
                $ldap_result = @ldap_search($ldapconnection,
                                            $context,
                                            $ldap_search_pattern,
                                            $ldap_fields_wanted);
            } else {
                // Search only in this context
                $ldap_result = @ldap_list($ldapconnection,
                                          $context,
                                          $ldap_search_pattern,
                                          $ldap_fields_wanted);
            }

            if (!$ldap_result) {
                continue;
            }

            // 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($ldapconnection, $ldap_result);

            // LDAP libraries return an odd array, really. Fix it.
            $flat_records = array();
            for ($c = 0; $c < $records['count']; $c++) {
                array_push($flat_records, $records[$c]);
            }
            unset($records);

            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 resource $ldapconnection a valid LDAP connection
     * @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($ldapconnection, $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($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 resource $ldapconnection a valid LDAP connection
     * @param string $memberdn distinguished name to search
     * @return array with member groups' distinguished names (can be emtpy)
     */
    protected function ldap_find_user_groups($ldapconnection, $memberdn) {
        $groups = array();

        $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups);
        return $groups;
    }

    /**
     * Recursively process the groups the given member distinguished name
     * belongs to, adding them to the already processed groups array.
     *
     * @param resource $ldapconnection
     * @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($ldapconnection, $memberdn, &$membergroups) {
        $result = @ldap_read ($ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
        if (!$result) {
            return;
        }

        if ($entry = ldap_first_entry($ldapconnection, $result)) {
            do {
                $attributes = ldap_get_attributes($ldapconnection, $entry);
                for ($j = 0; $j < $attributes['count']; $j++) {
                    $groups = ldap_get_values_len($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($ldapconnection, $group, $membergroups);
                        }
                    }
                }
            }
            while ($entry = ldap_next_entry($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 resource $ldapconnection a valid LDAP connection
     * @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($ldapconnection, $group, $memberattribute) {
        switch ($this->get_config('user_type')) {
            case 'ad':
                // $group is already the distinguished name to search.
                $dn = $group;

                $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
                $entry = ldap_first_entry($ldapconnection, $result);
                $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass');

                if (!in_array('group', $objectclass)) {
                    // Not a group, so return immediately.
                    return array($group);
                }

                $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
                $entry = ldap_first_entry($ldapconnection, $result);
                $members = @ldap_get_values($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($ldapconnection, $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
     * NOTE: if you pass true for $skip_fix_course_sortorder
     * you will want to call fix_course_sortorder() after your are done
     * with course creation.
     *
     * @param array $course_ext
     * @param boolean $skip_fix_course_sortorder
     * @return mixed false on error, id for the newly created course otherwise.
     */
    function create_course($course_ext, $skip_fix_course_sortorder=false) {
        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')))) {
                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->numsections    = $courseconfig->numsections;
            $template->hiddensections = $courseconfig->hiddensections;
            $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!
            error_log($this->errorlogtag.get_string('cannotcreatecourse', 'enrol_ldap'));
            error_log($this->errorlogtag.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];
        }

        $newcourse = create_course($course);
        return $newcourse->id;
    }
}

