MDL-25813 fixed silly typo
[moodle.git] / enrol / ldap / lib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * LDAP enrolment plugin implementation.
20  *
21  * This plugin synchronises enrolment and roles with a LDAP server.
22  *
23  * @package    enrol
24  * @subpackage ldap
25  * @author     Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others
26  * @copyright  1999 onwards Martin Dougiamas {@link http://moodle.com}
27  * @copyright  2010 Iñaki Arenaza <iarenaza@eps.mondragon.edu>
28  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29  */
31 defined('MOODLE_INTERNAL') || die();
33 class enrol_ldap_plugin extends enrol_plugin {
34     protected $enrol_localcoursefield = 'idnumber';
35     protected $enroltype = 'enrol_ldap';
36     protected $errorlogtag = '[ENROL LDAP] ';
38     /**
39      * Constructor for the plugin. In addition to calling the parent
40      * constructor, we define and 'fix' some settings depending on the
41      * real settings the admin defined.
42      */
43     public function __construct() {
44         global $CFG;
45         require_once($CFG->libdir.'/ldaplib.php');
47         // Do our own stuff to fix the config (it's easier to do it
48         // here than using the admin settings infrastructure). We
49         // don't call $this->set_config() for any of the 'fixups'
50         // (except the objectclass, as it's critical) because the user
51         // didn't specify any values and relied on the default values
52         // defined for the user type she chose.
53         $this->load_config();
55         // Make sure we get sane defaults for critical values.
56         $this->config->ldapencoding = $this->get_config('ldapencoding', 'utf-8');
57         $this->config->user_type = $this->get_config('user_type', 'default');
59         $ldap_usertypes = ldap_supported_usertypes();
60         $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
61         unset($ldap_usertypes);
63         $default = ldap_getdefaults();
64         // Remove the objectclass default, as the values specified there are for
65         // users, and we are dealing with groups here.
66         unset($default['objectclass']);
68         // Use defaults if values not given. Dont use this->get_config()
69         // here to be able to check for 0 and false values too.
70         foreach ($default as $key => $value) {
71             // Watch out - 0, false are correct values too, so we can't use $this->get_config()
72             if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
73                 $this->config->{$key} = $value[$this->config->user_type];
74             }
75         }
77         if (empty($this->config->objectclass)) {
78             // Can't send empty filter. Fix it for now and future occasions
79             $this->set_config('objectclass', '(objectClass=*)');
80         } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
81             // Value is 'objectClass=some-string-here', so just add ()
82             // around the value (filter _must_ have them).
83             // Fix it for now and future occasions
84             $this->set_config('objectclass', '('.$this->config->objectclass.')');
85         } else if (stripos($this->config->objectclass, '(') !== 0) {
86             // Value is 'some-string-not-starting-with-left-parentheses',
87             // which is assumed to be the objectClass matching value.
88             // So build a valid filter with it.
89             $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')');
90         } else {
91             // There is an additional possible value
92             // '(some-string-here)', that can be used to specify any
93             // valid filter string, to select subsets of users based
94             // on any criteria. For example, we could select the users
95             // whose objectClass is 'user' and have the
96             // 'enabledMoodleUser' attribute, with something like:
97             //
98             //   (&(objectClass=user)(enabledMoodleUser=1))
99             //
100             // In this particular case we don't need to do anything,
101             // so leave $this->config->objectclass as is.
102         }
103     }
105     /**
106      * Is it possible to delete enrol instance via standard UI?
107      *
108      * @param object $instance
109      * @return bool
110      */
111     public function instance_deleteable($instance) {
112         if (!enrol_is_enabled('ldap')) {
113             return true;
114         }
116         if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
117             return true;
118         }
120         // TODO: connect to external system and make sure no users are to be enrolled in this course
121         return false;
122     }
124     /**
125      * Forces synchronisation of user enrolments with LDAP server.
126      * It creates courses if the plugin is configured to do so.
127      *
128      * @param object $user user record
129      * @return void
130      */
131     public function sync_user_enrolments($user) {
132         global $DB;
134         $ldapconnection = $this->ldap_connect();
135         if (!$ldapconnection) {
136             return;
137         }
139         if (!is_object($user) or !property_exists($user, 'id')) {
140             throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
141         }
143         if (!property_exists($user, 'idnumber')) {
144             debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
145             $user = $DB->get_record('user', array('id'=>$user->id));
146         }
148         // We may need a lot of memory here
149         @set_time_limit(0);
150         raise_memory_limit(MEMORY_HUGE);
152         // Get enrolments for each type of role.
153         $roles = get_all_roles();
154         $enrolments = array();
155         foreach($roles as $role) {
156             // Get external enrolments according to LDAP server
157             $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($ldapconnection, $user->idnumber, $role);
159             // Get the list of current user enrolments that come from LDAP
160             $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
161                      FROM {user} u
162                      JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
163                      JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
164                      JOIN {enrol} e ON (e.id = ue.enrolid)
165                      JOIN {course} c ON (c.id = e.courseid)
166                     WHERE u.deleted = 0 AND u.id = :userid";
167             $params = array ('roleid'=>$role->id, 'userid'=>$user->id);
168             $enrolments[$role->id]['current'] = $DB->get_records_sql($sql, $params);
169         }
171         $ignorehidden = $this->get_config('ignorehiddencourses');
172         $courseidnumber = $this->get_config('course_idnumber');
173         foreach($roles as $role) {
174             foreach ($enrolments[$role->id]['ext'] as $enrol) {
175                 $course_ext_id = $enrol[$courseidnumber][0];
176                 if (empty($course_ext_id)) {
177                     error_log($this->errorlogtag.get_string('extcourseidinvalid', 'enrol_ldap'));
178                     continue; // Next; skip this one!
179                 }
181                 // Create the course if required
182                 $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));
183                 if (empty($course)) { // Course doesn't exist
184                     if ($this->get_config('autocreate')) { // Autocreate
185                         error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
186                                                                 array('courseextid'=>$course_ext_id)));
187                         if ($newcourseid = $this->create_course($enrol)) {
188                             $course = $DB->get_record('course', array('id'=>$newcourseid));
189                         }
190                     } else {
191                         error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
192                                                                 array('courseextid'=>$course_ext_id)));
193                         continue; // Next; skip this one!
194                     }
195                 }
197                 // Deal with enrolment in the moodle db
198                 // Add necessary enrol instance if not present yet;
199                 $sql = "SELECT c.id, c.visible, e.id as enrolid
200                           FROM {course} c
201                           JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
202                          WHERE c.id = :courseid";
203                 $params = array('courseid'=>$course->id);
204                 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
205                     $course_instance = new stdClass();
206                     $course_instance->id = $course->id;
207                     $course_instance->visible = $course->visible;
208                     $course_instance->enrolid = $this->add_instance($course_instance);
209                 }
211                 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
212                     continue; // Weird; skip this one.
213                 }
215                 if ($ignorehidden && !$course_instance->visible) {
216                     continue;
217                 }
219                 if (empty($enrolments[$role->id]['current'][$course->id])) {
220                     // Enrol the user in the given course, with that role.
221                     $this->enrol_user($instance, $user->id, $role->id);
222                     // Make sure we set the enrolment status to active. If the user wasn't
223                     // previously enrolled to the course, enrol_user() sets it. But if we
224                     // configured the plugin to suspend the user enrolments _AND_ remove
225                     // the role assignments on external unenrol, then enrol_user() doesn't
226                     // set it back to active on external re-enrolment. So set it
227                     // unconditionnally to cover both cases.
228                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
229                     error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
230                                                             array('user_username'=> $user->username,
231                                                                   'course_shortname'=>$course->shortname,
232                                                                   'course_id'=>$course->id)));
233                 } else {
234                     if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {
235                         // Reenable enrolment that was previously disabled. Enrolment refreshed
236                         $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
237                         error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
238                                                                 array('user_username'=> $user->username,
239                                                                       'course_shortname'=>$course->shortname,
240                                                                       'course_id'=>$course->id)));
241                     }
242                 }
244                 // Remove this course from the current courses, to be able to detect
245                 // which current courses should be unenroled from when we finish processing
246                 // external enrolments.
247                 unset($enrolments[$role->id]['current'][$course->id]);
248             }
250             // Deal with unenrolments.
251             $transaction = $DB->start_delegated_transaction();
252             foreach ($enrolments[$role->id]['current'] as $course) {
253                 $context = get_context_instance(CONTEXT_COURSE, $course->courseid);
254                 $instance = $DB->get_record('enrol', array('id'=>$course->enrolid));
255                 switch ($this->get_config('unenrolaction')) {
256                     case ENROL_EXT_REMOVED_UNENROL:
257                         $this->unenrol_user($instance, $user->id);
258                         error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
259                                                                 array('user_username'=> $user->username,
260                                                                       'course_shortname'=>$course->shortname,
261                                                                       'course_id'=>$course->courseid)));
262                         break;
263                     case ENROL_EXT_REMOVED_KEEP:
264                         // Keep - only adding enrolments
265                         break;
266                     case ENROL_EXT_REMOVED_SUSPEND:
267                         if ($course->status != ENROL_USER_SUSPENDED) {
268                             $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
269                             error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
270                                                                     array('user_username'=> $user->username,
271                                                                           'course_shortname'=>$course->shortname,
272                                                                           'course_id'=>$course->courseid)));
273                         }
274                         break;
275                     case ENROL_EXT_REMOVED_SUSPENDNOROLES:
276                         if ($course->status != ENROL_USER_SUSPENDED) {
277                             $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
278                         }
279                         role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
280                         error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
281                                                                 array('user_username'=> $user->username,
282                                                                       'course_shortname'=>$course->shortname,
283                                                                       'course_id'=>$course->courseid)));
284                         break;
285                 }
286             }
287             $transaction->allow_commit();
288         }
290         $this->ldap_close($ldapconnection);
291     }
293     /**
294      * Forces synchronisation of all enrolments with LDAP server.
295      * It creates courses if the plugin is configured to do so.
296      *
297      * @return void
298      */
299     public function sync_enrolments() {
300         global $CFG, $DB;
302         $ldapconnection = $this->ldap_connect();
303         if (!$ldapconnection) {
304             return;
305         }
307         // we may need a lot of memory here
308         @set_time_limit(0);
309         raise_memory_limit(MEMORY_HUGE);
311         // Get enrolments for each type of role.
312         $roles = get_all_roles();
313         $enrolments = array();
314         foreach($roles as $role) {
315             // Get all contexts
316             $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});
318             // Get all the fields we will want for the potential course creation
319             // as they are light. Don't get membership -- potentially a lot of data.
320             $ldap_fields_wanted = array('dn', $this->config->course_idnumber);
321             if (!empty($this->config->course_fullname)) {
322                 array_push($ldap_fields_wanted, $this->config->course_fullname);
323             }
324             if (!empty($this->config->course_shortname)) {
325                 array_push($ldap_fields_wanted, $this->config->course_shortname);
326             }
327             if (!empty($this->config->course_summary)) {
328                 array_push($ldap_fields_wanted, $this->config->course_summary);
329             }
330             array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});
332             // Define the search pattern
333             $ldap_search_pattern = $this->config->objectclass;
335             foreach ($ldap_contexts as $ldap_context) {
336                 $ldap_context = trim($ldap_context);
337                 if (empty($ldap_context)) {
338                     continue; // Next;
339                 }
341                 if ($this->config->course_search_sub) {
342                     // Use ldap_search to find first user from subtree
343                     $ldap_result = @ldap_search($ldapconnection,
344                                                 $ldap_context,
345                                                 $ldap_search_pattern,
346                                                 $ldap_fields_wanted);
347                 } else {
348                     // Search only in this context
349                     $ldap_result = @ldap_list($ldapconnection,
350                                               $ldap_context,
351                                               $ldap_search_pattern,
352                                               $ldap_fields_wanted);
353                 }
354                 if (!$ldap_result) {
355                     continue; // Next
356                 }
358                 // Check and push results
359                 $records = ldap_get_entries($ldapconnection, $ldap_result);
361                 // LDAP libraries return an odd array, really. fix it:
362                 $flat_records = array();
363                 for ($c = 0; $c < $records['count']; $c++) {
364                     array_push($flat_records, $records[$c]);
365                 }
366                 // Free some mem
367                 unset($records);
369                 if (count($flat_records)) {
370                     $ignorehidden = $this->get_config('ignorehiddencourses');
371                     foreach($flat_records as $course) {
372                         $course = array_change_key_case($course, CASE_LOWER);
373                         $idnumber = $course{$this->config->course_idnumber}[0];
374                         print_string('synccourserole', 'enrol_ldap',
375                                      array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname));
377                         // Does the course exist in moodle already?
378                         $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
379                         if (empty($course_obj)) { // Course doesn't exist
380                             if ($this->get_config('autocreate')) { // Autocreate
381                                 error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
382                                                                         array('courseextid'=>$idnumber)));
383                                 if ($newcourseid = $this->create_course($course)) {
384                                     $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
385                                 }
386                             } else {
387                                 error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
388                                                                         array('courseextid'=>$idnumber)));
389                                 continue; // Next; skip this one!
390                             }
391                         }
393                         // Enrol & unenrol
395                         // Pull the ldap membership into a nice array
396                         // this is an odd array -- mix of hash and array --
397                         $ldapmembers = array();
399                         if (array_key_exists('memberattribute_role'.$role->id, $this->config)
400                             && !empty($this->config->{'memberattribute_role'.$role->id})
401                             && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
403                             $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
404                             unset($ldapmembers['count']); // Remove oddity ;)
406                             // If we have enabled nested groups, we need to expand
407                             // the groups to get the real user list. We need to do
408                             // this before dealing with 'memberattribute_isdn'.
409                             if ($this->config->nested_groups) {
410                                 $users = array();
411                                 foreach ($ldapmembers as $ldapmember) {
412                                     $grpusers = $this->ldap_explode_group($ldapconnection,
413                                                                           $ldapmember,
414                                                                           $this->config->{'memberattribute_role'.$role->id});
416                                     $users = array_merge($users, $grpusers);
417                                 }
418                                 $ldapmembers = array_unique($users); // There might be duplicates.
419                             }
421                             // Deal with the case where the member attribute holds distinguished names,
422                             // but only if the user attribute is not a distinguished name itself.
423                             if ($this->config->memberattribute_isdn
424                                 && ($this->config->idnumber_attribute !== 'dn')
425                                 && ($this->config->idnumber_attribute !== 'distinguishedname')) {
426                                 // We need to retrieve the idnumber for all the users in $ldapmembers,
427                                 // as the idnumber does not match their dn and we get dn's from membership.
428                                 $memberidnumbers = array();
429                                 foreach ($ldapmembers as $ldapmember) {
430                                     $result = ldap_read($ldapconnection, $ldapmember, '(objectClass=*)',
431                                                         array($this->config->idnumber_attribute));
432                                     $entry = ldap_first_entry($ldapconnection, $result);
433                                     $values = ldap_get_values($ldapconnection, $entry, $this->config->idnumber_attribute);
434                                     array_push($memberidnumbers, $values[0]);
435                                 }
437                                 $ldapmembers = $memberidnumbers;
438                             }
439                         }
441                         // Prune old ldap enrolments
442                         // hopefully they'll fit in the max buffer size for the RDBMS
443                         $sql= "SELECT u.id as userid, u.username, ue.status,
444                                       ra.contextid, ra.itemid as instanceid
445                                  FROM {user} u
446                                  JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
447                                  JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
448                                  JOIN {enrol} e ON (e.id = ue.enrolid)
449                                 WHERE u.deleted = 0 AND e.courseid = :courseid ";
450                         $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
451                         if (!empty($ldapmembers)) {
452                             list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm0', false);
453                             $sql .= "AND u.idnumber $ldapml";
454                             $params = array_merge($params, $params2);
455                             unset($params2);
456                         } else {
457                             print_string('emptyenrolment', 'enrol_ldap',
458                                          array('role_shortname'=> $role->shortname,
459                                                'course_shortname'=>$course_obj->shortname));
460                         }
461                         $todelete = $DB->get_records_sql($sql, $params);
463                         $context = get_context_instance(CONTEXT_COURSE, $course_obj->id);
464                         if (!empty($todelete)) {
465                             $transaction = $DB->start_delegated_transaction();
466                             foreach ($todelete as $row) {
467                                 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
468                                 switch ($this->get_config('unenrolaction')) {
469                                 case ENROL_EXT_REMOVED_UNENROL:
470                                     $this->unenrol_user($instance, $row->userid);
471                                     error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
472                                                                             array('user_username'=> $row->username,
473                                                                                   'course_shortname'=>$course_obj->shortname,
474                                                                                   'course_id'=>$course_obj->id)));
475                                     break;
476                                 case ENROL_EXT_REMOVED_KEEP:
477                                     // Keep - only adding enrolments
478                                     break;
479                                 case ENROL_EXT_REMOVED_SUSPEND:
480                                     if ($row->status != ENROL_USER_SUSPENDED) {
481                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
482                                         error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
483                                                                                 array('user_username'=> $row->username,
484                                                                                       'course_shortname'=>$course_obj->shortname,
485                                                                                       'course_id'=>$course_obj->id)));
486                                     }
487                                     break;
488                                 case ENROL_EXT_REMOVED_SUSPENDNOROLES:
489                                     if ($row->status != ENROL_USER_SUSPENDED) {
490                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
491                                     }
492                                     role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
493                                     error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
494                                                                             array('user_username'=> $row->username,
495                                                                                   'course_shortname'=>$course_obj->shortname,
496                                                                                   'course_id'=>$course_obj->id)));
497                                     break;
498                                 }
499                             }
500                             $transaction->allow_commit();
501                         }
503                         // Insert current enrolments
504                         // bad we can't do INSERT IGNORE with postgres...
506                         // Add necessary enrol instance if not present yet;
507                         $sql = "SELECT c.id, c.visible, e.id as enrolid
508                                   FROM {course} c
509                                   JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
510                                  WHERE c.id = :courseid";
511                         $params = array('courseid'=>$course_obj->id);
512                         if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
513                             $course_instance = new stdClass();
514                             $course_instance->id = $course_obj->id;
515                             $course_instance->visible = $course_obj->visible;
516                             $course_instance->enrolid = $this->add_instance($course_instance);
517                         }
519                         if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
520                             continue; // Weird; skip this one.
521                         }
523                         if ($ignorehidden && !$course_instance->visible) {
524                             continue;
525                         }
527                         $transaction = $DB->start_delegated_transaction();
528                         foreach ($ldapmembers as $ldapmember) {
529                             $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
530                             $member = $DB->get_record_sql($sql, array($ldapmember));
531                             if(empty($member) || empty($member->id)){
532                                 print_string ('couldnotfinduser', 'enrol_ldap', $ldapmember);
533                                 continue;
534                             }
536                             $sql= "SELECT ue.status
537                                      FROM {user_enrolments} ue
538                                      JOIN {enrol} e ON (e.id = ue.enrolid)
539                                      JOIN {role_assignments} ra ON (ra.itemid = e.id AND ra.component = 'enrol_ldap')
540                                     WHERE e.courseid = :courseid AND ue.userid = :userid";
541                             $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
542                             $userenrolment = $DB->get_record_sql($sql, $params);
544                             if(empty($userenrolment)) {
545                                 $this->enrol_user($instance, $member->id, $role->id);
546                                 // Make sure we set the enrolment status to active. If the user wasn't
547                                 // previously enrolled to the course, enrol_user() sets it. But if we
548                                 // configured the plugin to suspend the user enrolments _AND_ remove
549                                 // the role assignments on external unenrol, then enrol_user() doesn't
550                                 // set it back to active on external re-enrolment. So set it
551                                 // unconditionnally to cover both cases.
552                                 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
553                                 error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
554                                                                         array('user_username'=> $member->username,
555                                                                               'course_shortname'=>$course_obj->shortname,
556                                                                               'course_id'=>$course_obj->id)));
558                             } else {
559                                 if ($userenrolment->status == ENROL_USER_SUSPENDED) {
560                                     // Reenable enrolment that was previously disabled. Enrolment refreshed
561                                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
562                                     error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
563                                                                             array('user_username'=> $member->username,
564                                                                                   'course_shortname'=>$course_obj->shortname,
565                                                                                   'course_id'=>$course_obj->id)));
566                                 }
567                             }
568                         }
569                         $transaction->allow_commit();
570                     }
571                 }
572             }
573         }
574         @$this->ldap_close();
575     }
577     /**
578      * Connect to the LDAP server, using the plugin configured
579      * settings. It's actually a wrapper around ldap_connect_moodle()
580      *
581      * @return mixed A valid LDAP connection or false.
582      */
583     protected function ldap_connect() {
584         global $CFG;
585         require_once($CFG->libdir.'/ldaplib.php');
587         // Cache ldap connections. They are expensive to set up
588         // and can drain the TCP/IP ressources on the server if we
589         // are syncing a lot of users (as we try to open a new connection
590         // to get the user details). This is the least invasive way
591         // to reuse existing connections without greater code surgery.
592         if(!empty($this->ldapconnection)) {
593             $this->ldapconns++;
594             return $this->ldapconnection;
595         }
597         if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
598                                                   $this->get_config('user_type'), $this->get_config('bind_dn'),
599                                                   $this->get_config('bind_pw'), $this->get_config('opt_deref'),
600                                                   $debuginfo)) {
601             $this->ldapconns = 1;
602             $this->ldapconnection = $ldapconnection;
603             return $ldapconnection;
604         }
606         // Log the problem, but don't show it to the user. She doesn't
607         // even have a chance to see it, as we redirect instantly to
608         // the user/front page.
609         error_log($this->errorlogtag.$debuginfo);
611         return false;
612     }
614     /**
615      * Disconnects from a LDAP server
616      *
617      */
618     protected function ldap_close() {
619         $this->ldapconns--;
620         if($this->ldapconns == 0) {
621             @ldap_close($this->ldapconnection);
622             unset($this->ldapconnection);
623         }
624     }
626     /**
627      * Return multidimensional array with details of user courses (at
628      * least dn and idnumber).
629      *
630      * @param resource $ldapconnection a valid LDAP connection.
631      * @param string $memberuid user idnumber (without magic quotes).
632      * @param object role is a record from the mdl_role table.
633      * @return array
634      */
635     protected function find_ext_enrolments ($ldapconnection, $memberuid, $role) {
636         global $CFG;
637         require_once($CFG->libdir.'/ldaplib.php');
639         if (empty($memberuid)) {
640             // No "idnumber" stored for this user, so no LDAP enrolments
641             return array();
642         }
644         $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
645         if (empty($ldap_contexts)) {
646             // No role contexts, so no LDAP enrolments
647             return array();
648         }
650         $textlib = textlib_get_instance();
651         $extmemberuid = $textlib->convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
653         if($this->get_config('memberattribute_isdn')) {
654             if (!($extmemberuid = $this->ldap_find_userdn ($ldapconnection, $extmemberuid))) {
655                 return array();
656             }
657         }
659         $ldap_search_pattern = '';
660         if($this->get_config('nested_groups')) {
661             $usergroups = $this->ldap_find_user_groups($ldapconnection, $extmemberuid);
662             if(count($usergroups) > 0) {
663                 foreach ($usergroups as $group) {
664                     $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
665                 }
666             }
667         }
669         // Default return value
670         $courses = array();
672         // Get all the fields we will want for the potential course creation
673         // as they are light. don't get membership -- potentially a lot of data.
674         $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
675         $fullname  = $this->get_config('course_fullname');
676         $shortname = $this->get_config('course_shortname');
677         $summary   = $this->get_config('course_summary');
678         if (isset($fullname)) {
679             array_push($ldap_fields_wanted, $fullname);
680         }
681         if (isset($shortname)) {
682             array_push($ldap_fields_wanted, $shortname);
683         }
684         if (isset($summary)) {
685             array_push($ldap_fields_wanted, $summary);
686         }
688         // Define the search pattern
689         if (empty($ldap_search_pattern)) {
690             $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
691         } else {
692             $ldap_search_pattern = '(|' . $ldap_search_pattern .
693                                        '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
694                                    ')';
695         }
696         $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
698         // Get all contexts and look for first matching user
699         $ldap_contexts = explode(';', $ldap_contexts);
700         foreach ($ldap_contexts as $context) {
701             $context = trim($context);
702             if (empty($context)) {
703                 continue;
704             }
706             if ($this->get_config('course_search_sub')) {
707                 // Use ldap_search to find first user from subtree
708                 $ldap_result = @ldap_search($ldapconnection,
709                                             $context,
710                                             $ldap_search_pattern,
711                                             $ldap_fields_wanted);
712             } else {
713                 // Search only in this context
714                 $ldap_result = @ldap_list($ldapconnection,
715                                           $context,
716                                           $ldap_search_pattern,
717                                           $ldap_fields_wanted);
718             }
720             if (!$ldap_result) {
721                 continue;
722             }
724             // Check and push results. ldap_get_entries() already
725             // lowercases the attribute index, so there's no need to
726             // use array_change_key_case() later.
727             $records = ldap_get_entries($ldapconnection, $ldap_result);
729             // LDAP libraries return an odd array, really. Fix it.
730             $flat_records = array();
731             for ($c = 0; $c < $records['count']; $c++) {
732                 array_push($flat_records, $records[$c]);
733             }
734             unset($records);
736             if (count($flat_records)) {
737                 $courses = array_merge($courses, $flat_records);
738             }
739         }
741         return $courses;
742     }
744     /**
745      * Search specified contexts for the specified userid and return the
746      * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
747      * around ldap_find_userdn().
748      *
749      * @param resource $ldapconnection a valid LDAP connection
750      * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
751      * @return mixed the user dn or false
752      */
753     protected function ldap_find_userdn($ldapconnection, $userid) {
754         global $CFG;
755         require_once($CFG->libdir.'/ldaplib.php');
757         $ldap_contexts = explode(';', $this->get_config('user_contexts'));
758         $ldap_defaults = ldap_getdefaults();
760         return ldap_find_userdn($ldapconnection, $userid, $ldap_contexts,
761                                 '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
762                                 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
763     }
765     /**
766      * Find the groups a given distinguished name belongs to, both directly
767      * and indirectly via nested groups membership.
768      *
769      * @param resource $ldapconnection a valid LDAP connection
770      * @param string $memberdn distinguished name to search
771      * @return array with member groups' distinguished names (can be emtpy)
772      */
773     protected function ldap_find_user_groups($ldapconnection, $memberdn) {
774         $groups = array();
776         $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups);
777         return $groups;
778     }
780     /**
781      * Recursively process the groups the given member distinguished name
782      * belongs to, adding them to the already processed groups array.
783      *
784      * @param resource $ldapconnection
785      * @param string $memberdn distinguished name to search
786      * @param array reference &$membergroups array with already found
787      *                        groups, where we'll put the newly found
788      *                        groups.
789      */
790     protected function ldap_find_user_groups_recursively($ldapconnection, $memberdn, &$membergroups) {
791         $result = @ldap_read ($ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
792         if (!$result) {
793             return;
794         }
796         if ($entry = ldap_first_entry($ldapconnection, $result)) {
797             do {
798                 $attributes = ldap_get_attributes($ldapconnection, $entry);
799                 for ($j = 0; $j < $attributes['count']; $j++) {
800                     $groups = ldap_get_values_len($ldapconnection, $entry, $attributes[$j]);
801                     foreach ($groups as $key => $group) {
802                         if ($key === 'count') {  // Skip the entries count
803                             continue;
804                         }
805                         if(!in_array($group, $membergroups)) {
806                             // Only push and recurse if we haven't 'seen' this group before
807                             // to prevent loops (MS Active Directory allows them!!).
808                             array_push($membergroups, $group);
809                             $this->ldap_find_user_groups_recursively($ldapconnection, $group, $membergroups);
810                         }
811                     }
812                 }
813             }
814             while ($entry = ldap_next_entry($ldapconnection, $entry));
815         }
816     }
818     /**
819      * Given a group name (either a RDN or a DN), get the list of users
820      * belonging to that group. If the group has nested groups, expand all
821      * the intermediate groups and return the full list of users that
822      * directly or indirectly belong to the group.
823      *
824      * @param resource $ldapconnection a valid LDAP connection
825      * @param string $group the group name to search
826      * @param string $memberattibute the attribute that holds the members of the group
827      * @return array the list of users belonging to the group. If $group
828      *         is not actually a group, returns array($group).
829      */
830     protected function ldap_explode_group($ldapconnection, $group, $memberattribute) {
831         switch ($this->get_config('user_type')) {
832             case 'ad':
833                 // $group is already the distinguished name to search.
834                 $dn = $group;
836                 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
837                 $entry = ldap_first_entry($ldapconnection, $result);
838                 $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass');
840                 if (!in_array('group', $objectclass)) {
841                     // Not a group, so return immediately.
842                     return array($group);
843                 }
845                 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
846                 $entry = ldap_first_entry($ldapconnection, $result);
847                 $members = @ldap_get_values($ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
848                 if ($members['count'] == 0) {
849                     // There are no members in this group, return nothing.
850                     return array();
851                 }
852                 unset($members['count']);
854                 $users = array();
855                 foreach ($members as $member) {
856                     $group_members = $this->ldap_explode_group($ldapconnection, $member, $memberattribute);
857                     $users = array_merge($users, $group_members);
858                 }
860                 return ($users);
861                 break;
862             default:
863                 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
864                                                         $this->get_config('user_type_name')));
866                 return array($group);
867         }
868     }
870     /**
871      * Will create the moodle course from the template
872      * course_ext is an array as obtained from ldap -- flattened somewhat
873      * NOTE: if you pass true for $skip_fix_course_sortorder
874      * you will want to call fix_course_sortorder() after your are done
875      * with course creation.
876      *
877      * @param array $course_ext
878      * @param boolean $skip_fix_course_sortorder
879      * @return mixed false on error, id for the newly created course otherwise.
880      */
881     function create_course($course_ext, $skip_fix_course_sortorder=false) {
882         global $CFG, $DB;
884         require_once("$CFG->dirroot/course/lib.php");
886         // Override defaults with template course
887         $course = new stdClass();
888         if ($this->get_config('template')) {
889             if($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
890                 unset($template->id); // So we are clear to reinsert the record
891                 unset($template->fullname);
892                 unset($template->shortname);
893                 unset($template->idnumber);
894                 $course = $template;
895             }
896         }
898         $course->category = $this->get_config('category');
899         if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
900             $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
901             $first = reset($categories);
902             $course->category = $first->id;
903         }
905         // Override with required ext data
906         $course->idnumber  = $course_ext[$this->get_config('course_idnumber')][0];
907         $course->fullname  = $course_ext[$this->get_config('course_fullname')][0];
908         $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
909         if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
910             // We are in trouble!
911             error_log($this->errorlogtag.get_string('cannotcreatecourse', 'enrol_ldap'));
912             error_log($this->errorlogtag.var_export($course, true));
913             return false;
914         }
916         $summary = $this->get_config('course_summary');
917         if (!isset($summary) || empty($course_ext[$summary][0])) {
918             $course->summary = '';
919         } else {
920             $course->summary = $course_ext[$this->get_config('course_summary')][0];
921         }
923         $newcourse = create_course($course);
924         return $newcourse->id;
925     }