MDL-25813 fixed silly typo
[moodle.git] / enrol / ldap / lib.php
CommitLineData
5704585c
I
1<?php
2
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/>.
17
18/**
19 * LDAP enrolment plugin implementation.
20 *
21 * This plugin synchronises enrolment and roles with a LDAP server.
22 *
4a77c443
PS
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
5704585c
I
29 */
30
4a77c443
PS
31defined('MOODLE_INTERNAL') || die();
32
5704585c
I
33class enrol_ldap_plugin extends enrol_plugin {
34 protected $enrol_localcoursefield = 'idnumber';
35 protected $enroltype = 'enrol_ldap';
36 protected $errorlogtag = '[ENROL LDAP] ';
37
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');
46
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();
54
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');
58
59 $ldap_usertypes = ldap_supported_usertypes();
60 $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
61 unset($ldap_usertypes);
62
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']);
67
4a77c443 68 // Use defaults if values not given. Dont use this->get_config()
5704585c
I
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 }
76
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 }
104
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 }
115
116 if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
117 return true;
118 }
119
120 // TODO: connect to external system and make sure no users are to be enrolled in this course
121 return false;
122 }
123
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;
133
134 $ldapconnection = $this->ldap_connect();
135 if (!$ldapconnection) {
136 return;
137 }
138
07cd46d8
PS
139 if (!is_object($user) or !property_exists($user, 'id')) {
140 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
141 }
142
07f31e9f 143 if (!property_exists($user, 'idnumber')) {
07cd46d8
PS
144 debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
145 $user = $DB->get_record('user', array('id'=>$user->id));
146 }
147
5704585c
I
148 // We may need a lot of memory here
149 @set_time_limit(0);
346c5887 150 raise_memory_limit(MEMORY_HUGE);
5704585c
I
151
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);
158
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 }
170
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 }
180
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 }
196
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))) {
94b9c2e8 205 $course_instance = new stdClass();
5704585c
I
206 $course_instance->id = $course->id;
207 $course_instance->visible = $course->visible;
208 $course_instance->enrolid = $this->add_instance($course_instance);
209 }
210
211 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
212 continue; // Weird; skip this one.
213 }
214
215 if ($ignorehidden && !$course_instance->visible) {
216 continue;
217 }
218
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 }
243
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 }
249
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 }
289
290 $this->ldap_close($ldapconnection);
291 }
292
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;
301
302 $ldapconnection = $this->ldap_connect();
303 if (!$ldapconnection) {
304 return;
305 }
306
307 // we may need a lot of memory here
308 @set_time_limit(0);
346c5887 309 raise_memory_limit(MEMORY_HUGE);
5704585c
I
310
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});
317
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});
331
332 // Define the search pattern
333 $ldap_search_pattern = $this->config->objectclass;
334
335 foreach ($ldap_contexts as $ldap_context) {
336 $ldap_context = trim($ldap_context);
337 if (empty($ldap_context)) {
338 continue; // Next;
339 }
340
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 }
357
358 // Check and push results
359 $records = ldap_get_entries($ldapconnection, $ldap_result);
360
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);
368
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));
376
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 }
392
393 // Enrol & unenrol
394
395 // Pull the ldap membership into a nice array
396 // this is an odd array -- mix of hash and array --
397 $ldapmembers = array();
398
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!
402
403 $ldapmembers = $course[$this->config->{'memberattribute_role'.$role->id}];
404 unset($ldapmembers['count']); // Remove oddity ;)
405
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});
415
416 $users = array_merge($users, $grpusers);
417 }
418 $ldapmembers = array_unique($users); // There might be duplicates.
419 }
420
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 }
436
437 $ldapmembers = $memberidnumbers;
438 }
439 }
440
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);
462
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 }
502
503 // Insert current enrolments
504 // bad we can't do INSERT IGNORE with postgres...
505
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))) {
94b9c2e8 513 $course_instance = new stdClass();
5704585c
I
514 $course_instance->id = $course_obj->id;
515 $course_instance->visible = $course_obj->visible;
516 $course_instance->enrolid = $this->add_instance($course_instance);
517 }
518
519 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid))) {
520 continue; // Weird; skip this one.
521 }
522
523 if ($ignorehidden && !$course_instance->visible) {
524 continue;
525 }
526
4a77c443 527 $transaction = $DB->start_delegated_transaction();
5704585c
I
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 }
535
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);
543
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)));
557
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 }
576
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');
586
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 }
596
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 }
605
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);
610
611 return false;
612 }
613
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 }
625
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');
638
639 if (empty($memberuid)) {
640 // No "idnumber" stored for this user, so no LDAP enrolments
641 return array();
642 }
643
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 }
649
650 $textlib = textlib_get_instance();
651 $extmemberuid = $textlib->convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
652
653 if($this->get_config('memberattribute_isdn')) {
654 if (!($extmemberuid = $this->ldap_find_userdn ($ldapconnection, $extmemberuid))) {
655 return array();
656 }
657 }
658
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 }
668
669 // Default return value
670 $courses = array();
671
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 }
687
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.')';
697
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 }
705
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 }
719
720 if (!$ldap_result) {
721 continue;
722 }
723
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);
728
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);
735
736 if (count($flat_records)) {
737 $courses = array_merge($courses, $flat_records);
738 }
739 }
740
741 return $courses;
742 }
743
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');
756
757 $ldap_contexts = explode(';', $this->get_config('user_contexts'));
758 $ldap_defaults = ldap_getdefaults();
759
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 }
764
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();
775
776 $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups);
777 return $groups;
778 }
779
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 }
795
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 }
817
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;
835
836 $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
837 $entry = ldap_first_entry($ldapconnection, $result);
838 $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass');
839
840 if (!in_array('group', $objectclass)) {
034ef761 841 // Not a group, so return immediately.
5704585c
I
842 return array($group);
843 }
844
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']);
853
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 }
859
860 return ($users);
861 break;
862 default:
863 error_log($this->errorlogtag.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
864 $this->get_config('user_type_name')));
865
866 return array($group);
867 }
868 }
869
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;
883
884 require_once("$CFG->dirroot/course/lib.php");
885
886 // Override defaults with template course
94b9c2e8 887 $course = new stdClass();
5704585c
I
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 }
897
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 }
904
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 }
915
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 }
922
923 $newcourse = create_course($course);
924 return $newcourse->id;
925 }
926}
927