Merge branch 'wip_master_mdl-3941_add_support_for_LDAP_TLS' of git://github.com/iaren...
[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 = context_course::instance($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         $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
309         // we may need a lot of memory here
310         @set_time_limit(0);
311         raise_memory_limit(MEMORY_HUGE);
313         // Get enrolments for each type of role.
314         $roles = get_all_roles();
315         $enrolments = array();
316         foreach($roles as $role) {
317             // Get all contexts
318             $ldap_contexts = explode(';', $this->config->{'contexts_role'.$role->id});
320             // Get all the fields we will want for the potential course creation
321             // as they are light. Don't get membership -- potentially a lot of data.
322             $ldap_fields_wanted = array('dn', $this->config->course_idnumber);
323             if (!empty($this->config->course_fullname)) {
324                 array_push($ldap_fields_wanted, $this->config->course_fullname);
325             }
326             if (!empty($this->config->course_shortname)) {
327                 array_push($ldap_fields_wanted, $this->config->course_shortname);
328             }
329             if (!empty($this->config->course_summary)) {
330                 array_push($ldap_fields_wanted, $this->config->course_summary);
331             }
332             array_push($ldap_fields_wanted, $this->config->{'memberattribute_role'.$role->id});
334             // Define the search pattern
335             $ldap_search_pattern = $this->config->objectclass;
337             $ldap_cookie = '';
338             foreach ($ldap_contexts as $ldap_context) {
339                 $ldap_context = trim($ldap_context);
340                 if (empty($ldap_context)) {
341                     continue; // Next;
342                 }
344                 $flat_records = array();
345                 do {
346                     if ($ldap_pagedresults) {
347                         ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
348                     }
350                     if ($this->config->course_search_sub) {
351                         // Use ldap_search to find first user from subtree
352                         $ldap_result = @ldap_search($ldapconnection,
353                                                     $ldap_context,
354                                                     $ldap_search_pattern,
355                                                     $ldap_fields_wanted);
356                     } else {
357                         // Search only in this context
358                         $ldap_result = @ldap_list($ldapconnection,
359                                                   $ldap_context,
360                                                   $ldap_search_pattern,
361                                                   $ldap_fields_wanted);
362                     }
363                     if (!$ldap_result) {
364                         continue; // Next
365                     }
367                     if ($ldap_pagedresults) {
368                         ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
369                     }
371                     // Check and push results
372                     $records = ldap_get_entries($ldapconnection, $ldap_result);
374                     // LDAP libraries return an odd array, really. fix it:
375                     for ($c = 0; $c < $records['count']; $c++) {
376                         array_push($flat_records, $records[$c]);
377                     }
378                     // Free some mem
379                     unset($records);
380                 } while ($ldap_pagedresults && !empty($ldap_cookie));
382                 // If LDAP paged results were used, the current connection must be completely
383                 // closed and a new one created, to work without paged results from here on.
384                 if ($ldap_pagedresults) {
385                     $this->ldap_close(true);
386                     $ldapconnection = $this->ldap_connect();
387                 }
389                 if (count($flat_records)) {
390                     $ignorehidden = $this->get_config('ignorehiddencourses');
391                     foreach($flat_records as $course) {
392                         $course = array_change_key_case($course, CASE_LOWER);
393                         $idnumber = $course{$this->config->course_idnumber}[0];
394                         print_string('synccourserole', 'enrol_ldap',
395                                      array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname));
397                         // Does the course exist in moodle already?
398                         $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
399                         if (empty($course_obj)) { // Course doesn't exist
400                             if ($this->get_config('autocreate')) { // Autocreate
401                                 error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
402                                                                         array('courseextid'=>$idnumber)));
403                                 if ($newcourseid = $this->create_course($course)) {
404                                     $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
405                                 }
406                             } else {
407                                 error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
408                                                                         array('courseextid'=>$idnumber)));
409                                 continue; // Next; skip this one!
410                             }
411                         }
413                         // Enrol & unenrol
415                         // Pull the ldap membership into a nice array
416                         // this is an odd array -- mix of hash and array --
417                         $ldapmembers = array();
419                         if (array_key_exists('memberattribute_role'.$role->id, $this->config)
420                             && !empty($this->config->{'memberattribute_role'.$role->id})
421                             && !empty($course[$this->config->{'memberattribute_role'.$role->id}])) { // May have no membership!
423                             $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
424                             unset($ldapmembers['count']); // Remove oddity ;)
426                             // If we have enabled nested groups, we need to expand
427                             // the groups to get the real user list. We need to do
428                             // this before dealing with 'memberattribute_isdn'.
429                             if ($this->config->nested_groups) {
430                                 $users = array();
431                                 foreach ($ldapmembers as $ldapmember) {
432                                     $grpusers = $this->ldap_explode_group($ldapconnection,
433                                                                           $ldapmember,
434                                                                           $this->config->{'memberattribute_role'.$role->id});
436                                     $users = array_merge($users, $grpusers);
437                                 }
438                                 $ldapmembers = array_unique($users); // There might be duplicates.
439                             }
441                             // Deal with the case where the member attribute holds distinguished names,
442                             // but only if the user attribute is not a distinguished name itself.
443                             if ($this->config->memberattribute_isdn
444                                 && ($this->config->idnumber_attribute !== 'dn')
445                                 && ($this->config->idnumber_attribute !== 'distinguishedname')) {
446                                 // We need to retrieve the idnumber for all the users in $ldapmembers,
447                                 // as the idnumber does not match their dn and we get dn's from membership.
448                                 $memberidnumbers = array();
449                                 foreach ($ldapmembers as $ldapmember) {
450                                     $result = ldap_read($ldapconnection, $ldapmember, '(objectClass=*)',
451                                                         array($this->config->idnumber_attribute));
452                                     $entry = ldap_first_entry($ldapconnection, $result);
453                                     $values = ldap_get_values($ldapconnection, $entry, $this->config->idnumber_attribute);
454                                     array_push($memberidnumbers, $values[0]);
455                                 }
457                                 $ldapmembers = $memberidnumbers;
458                             }
459                         }
461                         // Prune old ldap enrolments
462                         // hopefully they'll fit in the max buffer size for the RDBMS
463                         $sql= "SELECT u.id as userid, u.username, ue.status,
464                                       ra.contextid, ra.itemid as instanceid
465                                  FROM {user} u
466                                  JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
467                                  JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
468                                  JOIN {enrol} e ON (e.id = ue.enrolid)
469                                 WHERE u.deleted = 0 AND e.courseid = :courseid ";
470                         $params = array('roleid'=>$role->id, 'courseid'=>$course_obj->id);
471                         $context = context_course::instance($course_obj->id);
472                         if (!empty($ldapmembers)) {
473                             list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED, 'm', false);
474                             $sql .= "AND u.idnumber $ldapml";
475                             $params = array_merge($params, $params2);
476                             unset($params2);
477                         } else {
478                             $shortname = format_string($course_obj->shortname, true, array('context' => $context));
479                             print_string('emptyenrolment', 'enrol_ldap',
480                                          array('role_shortname'=> $role->shortname,
481                                                'course_shortname' => $shortname));
482                         }
483                         $todelete = $DB->get_records_sql($sql, $params);
485                         if (!empty($todelete)) {
486                             $transaction = $DB->start_delegated_transaction();
487                             foreach ($todelete as $row) {
488                                 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid));
489                                 switch ($this->get_config('unenrolaction')) {
490                                 case ENROL_EXT_REMOVED_UNENROL:
491                                     $this->unenrol_user($instance, $row->userid);
492                                     error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
493                                                                             array('user_username'=> $row->username,
494                                                                                   'course_shortname'=>$course_obj->shortname,
495                                                                                   'course_id'=>$course_obj->id)));
496                                     break;
497                                 case ENROL_EXT_REMOVED_KEEP:
498                                     // Keep - only adding enrolments
499                                     break;
500                                 case ENROL_EXT_REMOVED_SUSPEND:
501                                     if ($row->status != ENROL_USER_SUSPENDED) {
502                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
503                                         error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
504                                                                                 array('user_username'=> $row->username,
505                                                                                       'course_shortname'=>$course_obj->shortname,
506                                                                                       'course_id'=>$course_obj->id)));
507                                     }
508                                     break;
509                                 case ENROL_EXT_REMOVED_SUSPENDNOROLES:
510                                     if ($row->status != ENROL_USER_SUSPENDED) {
511                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
512                                     }
513                                     role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
514                                     error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
515                                                                             array('user_username'=> $row->username,
516                                                                                   'course_shortname'=>$course_obj->shortname,
517                                                                                   'course_id'=>$course_obj->id)));
518                                     break;
519                                 }
520                             }
521                             $transaction->allow_commit();
522                         }
524                         // Insert current enrolments
525                         // bad we can't do INSERT IGNORE with postgres...
527                         // Add necessary enrol instance if not present yet;
528                         $sql = "SELECT c.id, c.visible, e.id as enrolid
529                                   FROM {course} c
530                                   JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
531                                  WHERE c.id = :courseid";
532                         $params = array('courseid'=>$course_obj->id);
533                         if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE))) {
534                             $course_instance = new stdClass();
535                             $course_instance->id = $course_obj->id;
536                             $course_instance->visible = $course_obj->visible;
537                             $course_instance->enrolid = $this->add_instance($course_instance);
538                         }
540                         if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
541                             continue; // Weird; skip this one.
542                         }
544                         if ($ignorehidden && !$course_instance->visible) {
545                             continue;
546                         }
548                         $transaction = $DB->start_delegated_transaction();
549                         foreach ($ldapmembers as $ldapmember) {
550                             $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
551                             $member = $DB->get_record_sql($sql, array($ldapmember));
552                             if(empty($member) || empty($member->id)){
553                                 print_string ('couldnotfinduser', 'enrol_ldap', $ldapmember);
554                                 continue;
555                             }
557                             $sql= "SELECT ue.status
558                                      FROM {user_enrolments} ue
559                                      JOIN {enrol} e ON (e.id = ue.enrolid)
560                                      JOIN {role_assignments} ra ON (ra.itemid = e.id AND ra.component = 'enrol_ldap')
561                                     WHERE e.courseid = :courseid AND ue.userid = :userid";
562                             $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
563                             $userenrolment = $DB->get_record_sql($sql, $params);
565                             if(empty($userenrolment)) {
566                                 $this->enrol_user($instance, $member->id, $role->id);
567                                 // Make sure we set the enrolment status to active. If the user wasn't
568                                 // previously enrolled to the course, enrol_user() sets it. But if we
569                                 // configured the plugin to suspend the user enrolments _AND_ remove
570                                 // the role assignments on external unenrol, then enrol_user() doesn't
571                                 // set it back to active on external re-enrolment. So set it
572                                 // unconditionnally to cover both cases.
573                                 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
574                                 error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
575                                                                         array('user_username'=> $member->username,
576                                                                               'course_shortname'=>$course_obj->shortname,
577                                                                               'course_id'=>$course_obj->id)));
579                             } else {
580                                 if ($userenrolment->status == ENROL_USER_SUSPENDED) {
581                                     // Reenable enrolment that was previously disabled. Enrolment refreshed
582                                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
583                                     error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
584                                                                             array('user_username'=> $member->username,
585                                                                                   'course_shortname'=>$course_obj->shortname,
586                                                                                   'course_id'=>$course_obj->id)));
587                                 }
588                             }
589                         }
590                         $transaction->allow_commit();
591                     }
592                 }
593             }
594         }
595         @$this->ldap_close();
596     }
598     /**
599      * Connect to the LDAP server, using the plugin configured
600      * settings. It's actually a wrapper around ldap_connect_moodle()
601      *
602      * @return mixed A valid LDAP connection or false.
603      */
604     protected function ldap_connect() {
605         global $CFG;
606         require_once($CFG->libdir.'/ldaplib.php');
608         // Cache ldap connections. They are expensive to set up
609         // and can drain the TCP/IP ressources on the server if we
610         // are syncing a lot of users (as we try to open a new connection
611         // to get the user details). This is the least invasive way
612         // to reuse existing connections without greater code surgery.
613         if(!empty($this->ldapconnection)) {
614             $this->ldapconns++;
615             return $this->ldapconnection;
616         }
618         if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
619                                                   $this->get_config('user_type'), $this->get_config('bind_dn'),
620                                                   $this->get_config('bind_pw'), $this->get_config('opt_deref'),
621                                                   $debuginfo, $this->get_config('start_tls'))) {
622             $this->ldapconns = 1;
623             $this->ldapconnection = $ldapconnection;
624             return $ldapconnection;
625         }
627         // Log the problem, but don't show it to the user. She doesn't
628         // even have a chance to see it, as we redirect instantly to
629         // the user/front page.
630         error_log($this->errorlogtag.$debuginfo);
632         return false;
633     }
635     /**
636      * Disconnects from a LDAP server
637      *
638      */
639     protected function ldap_close() {
640         $this->ldapconns--;
641         if($this->ldapconns == 0) {
642             @ldap_close($this->ldapconnection);
643             unset($this->ldapconnection);
644         }
645     }
647     /**
648      * Return multidimensional array with details of user courses (at
649      * least dn and idnumber).
650      *
651      * @param resource $ldapconnection a valid LDAP connection.
652      * @param string $memberuid user idnumber (without magic quotes).
653      * @param object role is a record from the mdl_role table.
654      * @return array
655      */
656     protected function find_ext_enrolments (&$ldapconnection, $memberuid, $role) {
657         global $CFG;
658         require_once($CFG->libdir.'/ldaplib.php');
660         if (empty($memberuid)) {
661             // No "idnumber" stored for this user, so no LDAP enrolments
662             return array();
663         }
665         $ldap_contexts = trim($this->get_config('contexts_role'.$role->id));
666         if (empty($ldap_contexts)) {
667             // No role contexts, so no LDAP enrolments
668             return array();
669         }
671         $extmemberuid = textlib::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
673         if($this->get_config('memberattribute_isdn')) {
674             if (!($extmemberuid = $this->ldap_find_userdn ($ldapconnection, $extmemberuid))) {
675                 return array();
676             }
677         }
679         $ldap_search_pattern = '';
680         if($this->get_config('nested_groups')) {
681             $usergroups = $this->ldap_find_user_groups($ldapconnection, $extmemberuid);
682             if(count($usergroups) > 0) {
683                 foreach ($usergroups as $group) {
684                     $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
685                 }
686             }
687         }
689         // Default return value
690         $courses = array();
692         // Get all the fields we will want for the potential course creation
693         // as they are light. don't get membership -- potentially a lot of data.
694         $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
695         $fullname  = $this->get_config('course_fullname');
696         $shortname = $this->get_config('course_shortname');
697         $summary   = $this->get_config('course_summary');
698         if (isset($fullname)) {
699             array_push($ldap_fields_wanted, $fullname);
700         }
701         if (isset($shortname)) {
702             array_push($ldap_fields_wanted, $shortname);
703         }
704         if (isset($summary)) {
705             array_push($ldap_fields_wanted, $summary);
706         }
708         // Define the search pattern
709         if (empty($ldap_search_pattern)) {
710             $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')';
711         } else {
712             $ldap_search_pattern = '(|' . $ldap_search_pattern .
713                                        '('.$this->get_config('memberattribute_role'.$role->id).'='.ldap_filter_addslashes($extmemberuid).')' .
714                                    ')';
715         }
716         $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
718         // Get all contexts and look for first matching user
719         $ldap_contexts = explode(';', $ldap_contexts);
720         $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
721         foreach ($ldap_contexts as $context) {
722             $context = trim($context);
723             if (empty($context)) {
724                 continue;
725             }
727             $ldap_cookie = '';
728             $flat_records = array();
729             do {
730                 if ($ldap_pagedresults) {
731                     ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
732                 }
734                 if ($this->get_config('course_search_sub')) {
735                     // Use ldap_search to find first user from subtree
736                     $ldap_result = @ldap_search($ldapconnection,
737                                                 $context,
738                                                 $ldap_search_pattern,
739                                                 $ldap_fields_wanted);
740                 } else {
741                     // Search only in this context
742                     $ldap_result = @ldap_list($ldapconnection,
743                                               $context,
744                                               $ldap_search_pattern,
745                                               $ldap_fields_wanted);
746                 }
748                 if (!$ldap_result) {
749                     continue;
750                 }
752                 if ($ldap_pagedresults) {
753                     ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
754                 }
756                 // Check and push results. ldap_get_entries() already
757                 // lowercases the attribute index, so there's no need to
758                 // use array_change_key_case() later.
759                 $records = ldap_get_entries($ldapconnection, $ldap_result);
761                 // LDAP libraries return an odd array, really. Fix it.
762                 for ($c = 0; $c < $records['count']; $c++) {
763                     array_push($flat_records, $records[$c]);
764                 }
765                 // Free some mem
766                 unset($records);
767             } while ($ldap_pagedresults && !empty($ldap_cookie));
769             // If LDAP paged results were used, the current connection must be completely
770             // closed and a new one created, to work without paged results from here on.
771             if ($ldap_pagedresults) {
772                 $this->ldap_close(true);
773                 $ldapconnection = $this->ldap_connect();
774             }
776             if (count($flat_records)) {
777                 $courses = array_merge($courses, $flat_records);
778             }
779         }
781         return $courses;
782     }
784     /**
785      * Search specified contexts for the specified userid and return the
786      * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
787      * around ldap_find_userdn().
788      *
789      * @param resource $ldapconnection a valid LDAP connection
790      * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
791      * @return mixed the user dn or false
792      */
793     protected function ldap_find_userdn($ldapconnection, $userid) {
794         global $CFG;
795         require_once($CFG->libdir.'/ldaplib.php');
797         $ldap_contexts = explode(';', $this->get_config('user_contexts'));
798         $ldap_defaults = ldap_getdefaults();
800         return ldap_find_userdn($ldapconnection, $userid, $ldap_contexts,
801                                 '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
802                                 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
803     }
805     /**
806      * Find the groups a given distinguished name belongs to, both directly
807      * and indirectly via nested groups membership.
808      *
809      * @param resource $ldapconnection a valid LDAP connection
810      * @param string $memberdn distinguished name to search
811      * @return array with member groups' distinguished names (can be emtpy)
812      */
813     protected function ldap_find_user_groups($ldapconnection, $memberdn) {
814         $groups = array();
816         $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups);
817         return $groups;
818     }
820     /**
821      * Recursively process the groups the given member distinguished name
822      * belongs to, adding them to the already processed groups array.
823      *
824      * @param resource $ldapconnection
825      * @param string $memberdn distinguished name to search
826      * @param array reference &$membergroups array with already found
827      *                        groups, where we'll put the newly found
828      *                        groups.
829      */
830     protected function ldap_find_user_groups_recursively($ldapconnection, $memberdn, &$membergroups) {
831         $result = @ldap_read($ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
832         if (!$result) {
833             return;
834         }
836         if ($entry = ldap_first_entry($ldapconnection, $result)) {
837             do {
838                 $attributes = ldap_get_attributes($ldapconnection, $entry);
839                 for ($j = 0; $j < $attributes['count']; $j++) {
840                     $groups = ldap_get_values_len($ldapconnection, $entry, $attributes[$j]);
841                     foreach ($groups as $key => $group) {
842                         if ($key === 'count') {  // Skip the entries count
843                             continue;
844                         }
845                         if(!in_array($group, $membergroups)) {
846                             // Only push and recurse if we haven't 'seen' this group before
847                             // to prevent loops (MS Active Directory allows them!!).
848                             array_push($membergroups, $group);
849                             $this->ldap_find_user_groups_recursively($ldapconnection, $group, $membergroups);
850                         }
851                     }
852                 }
853             }
854             while ($entry = ldap_next_entry($ldapconnection, $entry));
855         }
856     }
858     /**
859      * Given a group name (either a RDN or a DN), get the list of users
860      * belonging to that group. If the group has nested groups, expand all
861      * the intermediate groups and return the full list of users that
862      * directly or indirectly belong to the group.
863      *
864      * @param resource $ldapconnection a valid LDAP connection
865      * @param string $group the group name to search
866      * @param string $memberattibute the attribute that holds the members of the group
867      * @return array the list of users belonging to the group. If $group
868      *         is not actually a group, returns array($group).
869      */
870     protected function ldap_explode_group($ldapconnection, $group, $memberattribute) {
871         switch ($this->get_config('user_type')) {
872             case 'ad':
873                 // $group is already the distinguished name to search.
874                 $dn = $group;
876                 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
877                 $entry = ldap_first_entry($ldapconnection, $result);
878                 $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass');
880                 if (!in_array('group', $objectclass)) {
881                     // Not a group, so return immediately.
882                     return array($group);
883                 }
885                 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
886                 $entry = ldap_first_entry($ldapconnection, $result);
887                 $members = @ldap_get_values($ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
888                 if ($members['count'] == 0) {
889                     // There are no members in this group, return nothing.
890                     return array();
891                 }
892                 unset($members['count']);
894                 $users = array();
895                 foreach ($members as $member) {
896                     $group_members = $this->ldap_explode_group($ldapconnection, $member, $memberattribute);
897                     $users = array_merge($users, $group_members);
898                 }
900                 return ($users);
901                 break;
902             default:
903                 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
904                                                         $this->get_config('user_type_name')));
906                 return array($group);
907         }
908     }
910     /**
911      * Will create the moodle course from the template
912      * course_ext is an array as obtained from ldap -- flattened somewhat
913      * NOTE: if you pass true for $skip_fix_course_sortorder
914      * you will want to call fix_course_sortorder() after your are done
915      * with course creation.
916      *
917      * @param array $course_ext
918      * @param boolean $skip_fix_course_sortorder
919      * @return mixed false on error, id for the newly created course otherwise.
920      */
921     function create_course($course_ext, $skip_fix_course_sortorder=false) {
922         global $CFG, $DB;
924         require_once("$CFG->dirroot/course/lib.php");
926         // Override defaults with template course
927         $template = false;
928         if ($this->get_config('template')) {
929             if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
930                 $template = fullclone(course_get_format($template)->get_course());
931                 unset($template->id); // So we are clear to reinsert the record
932                 unset($template->fullname);
933                 unset($template->shortname);
934                 unset($template->idnumber);
935             }
936         }
937         if (!$template) {
938             $courseconfig = get_config('moodlecourse');
939             $template = new stdClass();
940             $template->summary        = '';
941             $template->summaryformat  = FORMAT_HTML;
942             $template->format         = $courseconfig->format;
943             $template->newsitems      = $courseconfig->newsitems;
944             $template->showgrades     = $courseconfig->showgrades;
945             $template->showreports    = $courseconfig->showreports;
946             $template->maxbytes       = $courseconfig->maxbytes;
947             $template->groupmode      = $courseconfig->groupmode;
948             $template->groupmodeforce = $courseconfig->groupmodeforce;
949             $template->visible        = $courseconfig->visible;
950             $template->lang           = $courseconfig->lang;
951             $template->groupmodeforce = $courseconfig->groupmodeforce;
952         }
953         $course = $template;
955         $course->category = $this->get_config('category');
956         if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
957             $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
958             $first = reset($categories);
959             $course->category = $first->id;
960         }
962         // Override with required ext data
963         $course->idnumber  = $course_ext[$this->get_config('course_idnumber')][0];
964         $course->fullname  = $course_ext[$this->get_config('course_fullname')][0];
965         $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
966         if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
967             // We are in trouble!
968             error_log($this->errorlogtag.get_string('cannotcreatecourse', 'enrol_ldap'));
969             error_log($this->errorlogtag.var_export($course, true));
970             return false;
971         }
973         $summary = $this->get_config('course_summary');
974         if (!isset($summary) || empty($course_ext[$summary][0])) {
975             $course->summary = '';
976         } else {
977             $course->summary = $course_ext[$this->get_config('course_summary')][0];
978         }
980         $newcourse = create_course($course);
981         return $newcourse->id;
982     }