2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * LDAP enrolment plugin implementation.
20 * This plugin synchronises enrolment and roles with a LDAP server.
23 * @author Iñaki Arenaza - based on code by Martin Dougiamas, Martin Langhoff and others
24 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
25 * @copyright 2010 Iñaki Arenaza <iarenaza@eps.mondragon.edu>
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29 defined('MOODLE_INTERNAL') ||
die();
31 class enrol_ldap_plugin
extends enrol_plugin
{
32 protected $enrol_localcoursefield = 'idnumber';
33 protected $enroltype = 'enrol_ldap';
34 protected $errorlogtag = '[ENROL LDAP] ';
37 * Constructor for the plugin. In addition to calling the parent
38 * constructor, we define and 'fix' some settings depending on the
39 * real settings the admin defined.
41 public function __construct() {
43 require_once($CFG->libdir
.'/ldaplib.php');
45 // Do our own stuff to fix the config (it's easier to do it
46 // here than using the admin settings infrastructure). We
47 // don't call $this->set_config() for any of the 'fixups'
48 // (except the objectclass, as it's critical) because the user
49 // didn't specify any values and relied on the default values
50 // defined for the user type she chose.
53 // Make sure we get sane defaults for critical values.
54 $this->config
->ldapencoding
= $this->get_config('ldapencoding', 'utf-8');
55 $this->config
->user_type
= $this->get_config('user_type', 'default');
57 $ldap_usertypes = ldap_supported_usertypes();
58 $this->config
->user_type_name
= $ldap_usertypes[$this->config
->user_type
];
59 unset($ldap_usertypes);
61 $default = ldap_getdefaults();
62 // Remove the objectclass default, as the values specified there are for
63 // users, and we are dealing with groups here.
64 unset($default['objectclass']);
66 // Use defaults if values not given. Dont use this->get_config()
67 // here to be able to check for 0 and false values too.
68 foreach ($default as $key => $value) {
69 // Watch out - 0, false are correct values too, so we can't use $this->get_config()
70 if (!isset($this->config
->{$key}) or $this->config
->{$key} == '') {
71 $this->config
->{$key} = $value[$this->config
->user_type
];
75 if (empty($this->config
->objectclass
)) {
76 // Can't send empty filter. Fix it for now and future occasions
77 $this->set_config('objectclass', '(objectClass=*)');
78 } else if (stripos($this->config
->objectclass
, 'objectClass=') === 0) {
79 // Value is 'objectClass=some-string-here', so just add ()
80 // around the value (filter _must_ have them).
81 // Fix it for now and future occasions
82 $this->set_config('objectclass', '('.$this->config
->objectclass
.')');
83 } else if (stripos($this->config
->objectclass
, '(') !== 0) {
84 // Value is 'some-string-not-starting-with-left-parentheses',
85 // which is assumed to be the objectClass matching value.
86 // So build a valid filter with it.
87 $this->set_config('objectclass', '(objectClass='.$this->config
->objectclass
.')');
89 // There is an additional possible value
90 // '(some-string-here)', that can be used to specify any
91 // valid filter string, to select subsets of users based
92 // on any criteria. For example, we could select the users
93 // whose objectClass is 'user' and have the
94 // 'enabledMoodleUser' attribute, with something like:
96 // (&(objectClass=user)(enabledMoodleUser=1))
98 // In this particular case we don't need to do anything,
99 // so leave $this->config->objectclass as is.
104 * Is it possible to delete enrol instance via standard UI?
106 * @param object $instance
109 public function can_delete_instance($instance) {
110 $context = context_course
::instance($instance->courseid
);
111 if (!has_capability('enrol/ldap:manage', $context)) {
115 if (!enrol_is_enabled('ldap')) {
119 if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
123 // TODO: connect to external system and make sure no users are to be enrolled in this course
128 * Is it possible to hide/show enrol instance via standard UI?
130 * @param stdClass $instance
133 public function can_hide_show_instance($instance) {
134 $context = context_course
::instance($instance->courseid
);
135 return has_capability('enrol/ldap:config', $context);
139 * Forces synchronisation of user enrolments with LDAP server.
140 * It creates courses if the plugin is configured to do so.
142 * @param object $user user record
145 public function sync_user_enrolments($user) {
148 // Do not try to print anything to the output because this method is called during interactive login.
150 $trace = new null_progress_trace();
152 $trace = new error_log_progress_trace($this->errorlogtag
);
155 if (!$this->ldap_connect($trace)) {
160 if (!is_object($user) or !property_exists($user, 'id')) {
161 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
164 if (!property_exists($user, 'idnumber')) {
165 debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
166 $user = $DB->get_record('user', array('id'=>$user->id
));
169 // We may need a lot of memory here
170 core_php_time_limit
::raise();
171 raise_memory_limit(MEMORY_HUGE
);
173 // Get enrolments for each type of role.
174 $roles = get_all_roles();
175 $enrolments = array();
176 foreach($roles as $role) {
177 // Get external enrolments according to LDAP server
178 $enrolments[$role->id
]['ext'] = $this->find_ext_enrolments($user->idnumber
, $role);
180 // Get the list of current user enrolments that come from LDAP
181 $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
183 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
184 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
185 JOIN {enrol} e ON (e.id = ue.enrolid)
186 JOIN {course} c ON (c.id = e.courseid)
187 WHERE u.deleted = 0 AND u.id = :userid";
188 $params = array ('roleid'=>$role->id
, 'userid'=>$user->id
);
189 $enrolments[$role->id
]['current'] = $DB->get_records_sql($sql, $params);
192 $ignorehidden = $this->get_config('ignorehiddencourses');
193 $courseidnumber = $this->get_config('course_idnumber');
194 foreach($roles as $role) {
195 foreach ($enrolments[$role->id
]['ext'] as $enrol) {
196 $course_ext_id = $enrol[$courseidnumber][0];
197 if (empty($course_ext_id)) {
198 $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
199 continue; // Next; skip this one!
202 // Create the course if required
203 $course = $DB->get_record('course', array($this->enrol_localcoursefield
=>$course_ext_id));
204 if (empty($course)) { // Course doesn't exist
205 if ($this->get_config('autocreate')) { // Autocreate
206 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
207 if (!$newcourseid = $this->create_course($enrol, $trace)) {
210 $course = $DB->get_record('course', array('id'=>$newcourseid));
212 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
213 continue; // Next; skip this one!
217 // Deal with enrolment in the moodle db
218 // Add necessary enrol instance if not present yet;
219 $sql = "SELECT c.id, c.visible, e.id as enrolid
221 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
222 WHERE c.id = :courseid";
223 $params = array('courseid'=>$course->id
);
224 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE
))) {
225 $course_instance = new stdClass();
226 $course_instance->id
= $course->id
;
227 $course_instance->visible
= $course->visible
;
228 $course_instance->enrolid
= $this->add_instance($course_instance);
231 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid
))) {
232 continue; // Weird; skip this one.
235 if ($ignorehidden && !$course_instance->visible
) {
239 if (empty($enrolments[$role->id
]['current'][$course->id
])) {
240 // Enrol the user in the given course, with that role.
241 $this->enrol_user($instance, $user->id
, $role->id
);
242 // Make sure we set the enrolment status to active. If the user wasn't
243 // previously enrolled to the course, enrol_user() sets it. But if we
244 // configured the plugin to suspend the user enrolments _AND_ remove
245 // the role assignments on external unenrol, then enrol_user() doesn't
246 // set it back to active on external re-enrolment. So set it
247 // unconditionnally to cover both cases.
248 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
249 $trace->output(get_string('enroluser', 'enrol_ldap',
250 array('user_username'=> $user->username
,
251 'course_shortname'=>$course->shortname
,
252 'course_id'=>$course->id
)));
254 if ($enrolments[$role->id
]['current'][$course->id
]->status
== ENROL_USER_SUSPENDED
) {
255 // Reenable enrolment that was previously disabled. Enrolment refreshed
256 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
257 $trace->output(get_string('enroluserenable', 'enrol_ldap',
258 array('user_username'=> $user->username
,
259 'course_shortname'=>$course->shortname
,
260 'course_id'=>$course->id
)));
264 // Remove this course from the current courses, to be able to detect
265 // which current courses should be unenroled from when we finish processing
266 // external enrolments.
267 unset($enrolments[$role->id
]['current'][$course->id
]);
270 // Deal with unenrolments.
271 $transaction = $DB->start_delegated_transaction();
272 foreach ($enrolments[$role->id
]['current'] as $course) {
273 $context = context_course
::instance($course->courseid
);
274 $instance = $DB->get_record('enrol', array('id'=>$course->enrolid
));
275 switch ($this->get_config('unenrolaction')) {
276 case ENROL_EXT_REMOVED_UNENROL
:
277 $this->unenrol_user($instance, $user->id
);
278 $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
279 array('user_username'=> $user->username
,
280 'course_shortname'=>$course->shortname
,
281 'course_id'=>$course->courseid
)));
283 case ENROL_EXT_REMOVED_KEEP
:
284 // Keep - only adding enrolments
286 case ENROL_EXT_REMOVED_SUSPEND
:
287 if ($course->status
!= ENROL_USER_SUSPENDED
) {
288 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
289 $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
290 array('user_username'=> $user->username
,
291 'course_shortname'=>$course->shortname
,
292 'course_id'=>$course->courseid
)));
295 case ENROL_EXT_REMOVED_SUSPENDNOROLES
:
296 if ($course->status
!= ENROL_USER_SUSPENDED
) {
297 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
299 role_unassign_all(array('contextid'=>$context->id
, 'userid'=>$user->id
, 'component'=>'enrol_ldap', 'itemid'=>$instance->id
));
300 $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
301 array('user_username'=> $user->username
,
302 'course_shortname'=>$course->shortname
,
303 'course_id'=>$course->courseid
)));
307 $transaction->allow_commit();
316 * Forces synchronisation of all enrolments with LDAP server.
317 * It creates courses if the plugin is configured to do so.
319 * @param progress_trace $trace
320 * @param int|null $onecourse limit sync to one course->id, null if all courses
323 public function sync_enrolments(progress_trace
$trace, $onecourse = null) {
326 if (!$this->ldap_connect($trace)) {
331 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
333 // we may need a lot of memory here
334 core_php_time_limit
::raise();
335 raise_memory_limit(MEMORY_HUGE
);
339 if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield
)) {
340 // Course does not exist, nothing to do.
341 $trace->output("Requested course $onecourse does not exist, no sync performed.");
345 if (empty($course->{$this->enrol_localcoursefield
})) {
346 $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");
350 $oneidnumber = ldap_filter_addslashes(core_text
::convert($course->idnumber
, 'utf-8', $this->get_config('ldapencoding')));
353 // Get enrolments for each type of role.
354 $roles = get_all_roles();
355 $enrolments = array();
356 foreach($roles as $role) {
358 $ldap_contexts = explode(';', $this->config
->{'contexts_role'.$role->id
});
360 // Get all the fields we will want for the potential course creation
361 // as they are light. Don't get membership -- potentially a lot of data.
362 $ldap_fields_wanted = array('dn', $this->config
->course_idnumber
);
363 if (!empty($this->config
->course_fullname
)) {
364 array_push($ldap_fields_wanted, $this->config
->course_fullname
);
366 if (!empty($this->config
->course_shortname
)) {
367 array_push($ldap_fields_wanted, $this->config
->course_shortname
);
369 if (!empty($this->config
->course_summary
)) {
370 array_push($ldap_fields_wanted, $this->config
->course_summary
);
372 array_push($ldap_fields_wanted, $this->config
->{'memberattribute_role'.$role->id
});
374 // Define the search pattern
375 $ldap_search_pattern = $this->config
->objectclass
;
377 if ($oneidnumber !== null) {
378 $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
382 foreach ($ldap_contexts as $ldap_context) {
383 $ldap_context = trim($ldap_context);
384 if (empty($ldap_context)) {
388 $flat_records = array();
390 if ($ldap_pagedresults) {
391 ldap_control_paged_result($this->ldapconnection
, $this->config
->pagesize
, true, $ldap_cookie);
394 if ($this->config
->course_search_sub
) {
395 // Use ldap_search to find first user from subtree
396 $ldap_result = @ldap_search
($this->ldapconnection
,
398 $ldap_search_pattern,
399 $ldap_fields_wanted);
401 // Search only in this context
402 $ldap_result = @ldap_list
($this->ldapconnection
,
404 $ldap_search_pattern,
405 $ldap_fields_wanted);
411 if ($ldap_pagedresults) {
412 ldap_control_paged_result_response($this->ldapconnection
, $ldap_result, $ldap_cookie);
415 // Check and push results
416 $records = ldap_get_entries($this->ldapconnection
, $ldap_result);
418 // LDAP libraries return an odd array, really. fix it:
419 for ($c = 0; $c < $records['count']; $c++
) {
420 array_push($flat_records, $records[$c]);
424 } while ($ldap_pagedresults && !empty($ldap_cookie));
426 // If LDAP paged results were used, the current connection must be completely
427 // closed and a new one created, to work without paged results from here on.
428 if ($ldap_pagedresults) {
430 $this->ldap_connect($trace);
433 if (count($flat_records)) {
434 $ignorehidden = $this->get_config('ignorehiddencourses');
435 foreach($flat_records as $course) {
436 $course = array_change_key_case($course, CASE_LOWER
);
437 $idnumber = $course{$this->config
->course_idnumber
}[0];
438 $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname
)));
440 // Does the course exist in moodle already?
441 $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield
=>$idnumber));
442 if (empty($course_obj)) { // Course doesn't exist
443 if ($this->get_config('autocreate')) { // Autocreate
444 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
445 if (!$newcourseid = $this->create_course($course, $trace)) {
448 $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
450 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
451 continue; // Next; skip this one!
453 } else { // Check if course needs update & update as needed.
454 $this->update_course($course_obj, $course, $trace);
459 // Pull the ldap membership into a nice array
460 // this is an odd array -- mix of hash and array --
461 $ldapmembers = array();
463 if (array_key_exists('memberattribute_role'.$role->id
, $this->config
)
464 && !empty($this->config
->{'memberattribute_role'.$role->id
})
465 && !empty($course[$this->config
->{'memberattribute_role'.$role->id
}])) { // May have no membership!
467 $ldapmembers = $course[$this->config
->{'memberattribute_role'.$role->id
}];
468 unset($ldapmembers['count']); // Remove oddity ;)
470 // If we have enabled nested groups, we need to expand
471 // the groups to get the real user list. We need to do
472 // this before dealing with 'memberattribute_isdn'.
473 if ($this->config
->nested_groups
) {
475 foreach ($ldapmembers as $ldapmember) {
476 $grpusers = $this->ldap_explode_group($ldapmember,
477 $this->config
->{'memberattribute_role'.$role->id
});
479 $users = array_merge($users, $grpusers);
481 $ldapmembers = array_unique($users); // There might be duplicates.
484 // Deal with the case where the member attribute holds distinguished names,
485 // but only if the user attribute is not a distinguished name itself.
486 if ($this->config
->memberattribute_isdn
487 && ($this->config
->idnumber_attribute
!== 'dn')
488 && ($this->config
->idnumber_attribute
!== 'distinguishedname')) {
489 // We need to retrieve the idnumber for all the users in $ldapmembers,
490 // as the idnumber does not match their dn and we get dn's from membership.
491 $memberidnumbers = array();
492 foreach ($ldapmembers as $ldapmember) {
493 $result = ldap_read($this->ldapconnection
, $ldapmember, '(objectClass=*)',
494 array($this->config
->idnumber_attribute
));
495 $entry = ldap_first_entry($this->ldapconnection
, $result);
496 $values = ldap_get_values($this->ldapconnection
, $entry, $this->config
->idnumber_attribute
);
497 array_push($memberidnumbers, $values[0]);
500 $ldapmembers = $memberidnumbers;
504 // Prune old ldap enrolments
505 // hopefully they'll fit in the max buffer size for the RDBMS
506 $sql= "SELECT u.id as userid, u.username, ue.status,
507 ra.contextid, ra.itemid as instanceid
509 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
510 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
511 JOIN {enrol} e ON (e.id = ue.enrolid)
512 WHERE u.deleted = 0 AND e.courseid = :courseid ";
513 $params = array('roleid'=>$role->id
, 'courseid'=>$course_obj->id
);
514 $context = context_course
::instance($course_obj->id
);
515 if (!empty($ldapmembers)) {
516 list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED
, 'm', false);
517 $sql .= "AND u.idnumber $ldapml";
518 $params = array_merge($params, $params2);
521 $shortname = format_string($course_obj->shortname
, true, array('context' => $context));
522 $trace->output(get_string('emptyenrolment', 'enrol_ldap',
523 array('role_shortname'=> $role->shortname
,
524 'course_shortname' => $shortname)));
526 $todelete = $DB->get_records_sql($sql, $params);
528 if (!empty($todelete)) {
529 $transaction = $DB->start_delegated_transaction();
530 foreach ($todelete as $row) {
531 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid
));
532 switch ($this->get_config('unenrolaction')) {
533 case ENROL_EXT_REMOVED_UNENROL
:
534 $this->unenrol_user($instance, $row->userid
);
535 $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
536 array('user_username'=> $row->username
,
537 'course_shortname'=>$course_obj->shortname
,
538 'course_id'=>$course_obj->id
)));
540 case ENROL_EXT_REMOVED_KEEP
:
541 // Keep - only adding enrolments
543 case ENROL_EXT_REMOVED_SUSPEND
:
544 if ($row->status
!= ENROL_USER_SUSPENDED
) {
545 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$row->userid
));
546 $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
547 array('user_username'=> $row->username
,
548 'course_shortname'=>$course_obj->shortname
,
549 'course_id'=>$course_obj->id
)));
552 case ENROL_EXT_REMOVED_SUSPENDNOROLES
:
553 if ($row->status
!= ENROL_USER_SUSPENDED
) {
554 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$row->userid
));
556 role_unassign_all(array('contextid'=>$row->contextid
, 'userid'=>$row->userid
, 'component'=>'enrol_ldap', 'itemid'=>$instance->id
));
557 $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
558 array('user_username'=> $row->username
,
559 'course_shortname'=>$course_obj->shortname
,
560 'course_id'=>$course_obj->id
)));
564 $transaction->allow_commit();
567 // Insert current enrolments
568 // bad we can't do INSERT IGNORE with postgres...
570 // Add necessary enrol instance if not present yet;
571 $sql = "SELECT c.id, c.visible, e.id as enrolid
573 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
574 WHERE c.id = :courseid";
575 $params = array('courseid'=>$course_obj->id
);
576 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE
))) {
577 $course_instance = new stdClass();
578 $course_instance->id
= $course_obj->id
;
579 $course_instance->visible
= $course_obj->visible
;
580 $course_instance->enrolid
= $this->add_instance($course_instance);
583 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid
))) {
584 continue; // Weird; skip this one.
587 if ($ignorehidden && !$course_instance->visible
) {
591 $transaction = $DB->start_delegated_transaction();
592 foreach ($ldapmembers as $ldapmember) {
593 $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
594 $member = $DB->get_record_sql($sql, array($ldapmember));
595 if(empty($member) ||
empty($member->id
)){
596 $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
600 $sql= "SELECT ue.status
601 FROM {user_enrolments} ue
602 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
603 WHERE e.courseid = :courseid AND ue.userid = :userid";
604 $params = array('courseid'=>$course_obj->id
, 'userid'=>$member->id
);
605 $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE
);
607 if (empty($userenrolment)) {
608 $this->enrol_user($instance, $member->id
, $role->id
);
609 // Make sure we set the enrolment status to active. If the user wasn't
610 // previously enrolled to the course, enrol_user() sets it. But if we
611 // configured the plugin to suspend the user enrolments _AND_ remove
612 // the role assignments on external unenrol, then enrol_user() doesn't
613 // set it back to active on external re-enrolment. So set it
614 // unconditionally to cover both cases.
615 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$member->id
));
616 $trace->output(get_string('enroluser', 'enrol_ldap',
617 array('user_username'=> $member->username
,
618 'course_shortname'=>$course_obj->shortname
,
619 'course_id'=>$course_obj->id
)));
622 if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id
, 'userid'=>$member->id
, 'contextid'=>$context->id
, 'component'=>'enrol_ldap', 'itemid'=>$instance->id
))) {
623 // This happens when reviving users or when user has multiple roles in one course.
624 $context = context_course
::instance($course_obj->id
);
625 role_assign($role->id
, $member->id
, $context->id
, 'enrol_ldap', $instance->id
);
626 $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
628 if ($userenrolment->status
== ENROL_USER_SUSPENDED
) {
629 // Reenable enrolment that was previously disabled. Enrolment refreshed
630 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$member->id
));
631 $trace->output(get_string('enroluserenable', 'enrol_ldap',
632 array('user_username'=> $member->username
,
633 'course_shortname'=>$course_obj->shortname
,
634 'course_id'=>$course_obj->id
)));
638 $transaction->allow_commit();
643 @$this->ldap_close();
648 * Connect to the LDAP server, using the plugin configured
649 * settings. It's actually a wrapper around ldap_connect_moodle()
651 * @param progress_trace $trace
652 * @return bool success
654 protected function ldap_connect(progress_trace
$trace = null) {
656 require_once($CFG->libdir
.'/ldaplib.php');
658 if (isset($this->ldapconnection
)) {
662 if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
663 $this->get_config('user_type'), $this->get_config('bind_dn'),
664 $this->get_config('bind_pw'), $this->get_config('opt_deref'),
665 $debuginfo, $this->get_config('start_tls'))) {
666 $this->ldapconnection
= $ldapconnection;
671 $trace->output($debuginfo);
673 error_log($this->errorlogtag
.$debuginfo);
680 * Disconnects from a LDAP server
683 protected function ldap_close() {
684 if (isset($this->ldapconnection
)) {
685 @ldap_close
($this->ldapconnection
);
686 $this->ldapconnection
= null;
692 * Return multidimensional array with details of user courses (at
693 * least dn and idnumber).
695 * @param string $memberuid user idnumber (without magic quotes).
696 * @param object role is a record from the mdl_role table.
699 protected function find_ext_enrolments($memberuid, $role) {
701 require_once($CFG->libdir
.'/ldaplib.php');
703 if (empty($memberuid)) {
704 // No "idnumber" stored for this user, so no LDAP enrolments
708 $ldap_contexts = trim($this->get_config('contexts_role'.$role->id
));
709 if (empty($ldap_contexts)) {
710 // No role contexts, so no LDAP enrolments
714 $extmemberuid = core_text
::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
716 if($this->get_config('memberattribute_isdn')) {
717 if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
722 $ldap_search_pattern = '';
723 if($this->get_config('nested_groups')) {
724 $usergroups = $this->ldap_find_user_groups($extmemberuid);
725 if(count($usergroups) > 0) {
726 foreach ($usergroups as $group) {
727 $group = ldap_filter_addslashes($group);
728 $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id
).'='.$group.')';
733 // Default return value
736 // Get all the fields we will want for the potential course creation
737 // as they are light. don't get membership -- potentially a lot of data.
738 $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
739 $fullname = $this->get_config('course_fullname');
740 $shortname = $this->get_config('course_shortname');
741 $summary = $this->get_config('course_summary');
742 if (isset($fullname)) {
743 array_push($ldap_fields_wanted, $fullname);
745 if (isset($shortname)) {
746 array_push($ldap_fields_wanted, $shortname);
748 if (isset($summary)) {
749 array_push($ldap_fields_wanted, $summary);
752 // Define the search pattern
753 if (empty($ldap_search_pattern)) {
754 $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id
).'='.ldap_filter_addslashes($extmemberuid).')';
756 $ldap_search_pattern = '(|' . $ldap_search_pattern .
757 '('.$this->get_config('memberattribute_role'.$role->id
).'='.ldap_filter_addslashes($extmemberuid).')' .
760 $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
762 // Get all contexts and look for first matching user
763 $ldap_contexts = explode(';', $ldap_contexts);
764 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
765 foreach ($ldap_contexts as $context) {
766 $context = trim($context);
767 if (empty($context)) {
772 $flat_records = array();
774 if ($ldap_pagedresults) {
775 ldap_control_paged_result($this->ldapconnection
, $this->config
->pagesize
, true, $ldap_cookie);
778 if ($this->get_config('course_search_sub')) {
779 // Use ldap_search to find first user from subtree
780 $ldap_result = @ldap_search
($this->ldapconnection
,
782 $ldap_search_pattern,
783 $ldap_fields_wanted);
785 // Search only in this context
786 $ldap_result = @ldap_list
($this->ldapconnection
,
788 $ldap_search_pattern,
789 $ldap_fields_wanted);
796 if ($ldap_pagedresults) {
797 ldap_control_paged_result_response($this->ldapconnection
, $ldap_result, $ldap_cookie);
800 // Check and push results. ldap_get_entries() already
801 // lowercases the attribute index, so there's no need to
802 // use array_change_key_case() later.
803 $records = ldap_get_entries($this->ldapconnection
, $ldap_result);
805 // LDAP libraries return an odd array, really. Fix it.
806 for ($c = 0; $c < $records['count']; $c++
) {
807 array_push($flat_records, $records[$c]);
811 } while ($ldap_pagedresults && !empty($ldap_cookie));
813 // If LDAP paged results were used, the current connection must be completely
814 // closed and a new one created, to work without paged results from here on.
815 if ($ldap_pagedresults) {
817 $this->ldap_connect();
820 if (count($flat_records)) {
821 $courses = array_merge($courses, $flat_records);
829 * Search specified contexts for the specified userid and return the
830 * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
831 * around ldap_find_userdn().
833 * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
834 * @return mixed the user dn or false
836 protected function ldap_find_userdn($userid) {
838 require_once($CFG->libdir
.'/ldaplib.php');
840 $ldap_contexts = explode(';', $this->get_config('user_contexts'));
841 $ldap_defaults = ldap_getdefaults();
843 return ldap_find_userdn($this->ldapconnection
, $userid, $ldap_contexts,
844 '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
845 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
849 * Find the groups a given distinguished name belongs to, both directly
850 * and indirectly via nested groups membership.
852 * @param string $memberdn distinguished name to search
853 * @return array with member groups' distinguished names (can be emtpy)
855 protected function ldap_find_user_groups($memberdn) {
858 $this->ldap_find_user_groups_recursively($memberdn, $groups);
863 * Recursively process the groups the given member distinguished name
864 * belongs to, adding them to the already processed groups array.
866 * @param string $memberdn distinguished name to search
867 * @param array reference &$membergroups array with already found
868 * groups, where we'll put the newly found
871 protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
872 $result = @ldap_read
($this->ldapconnection
, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
877 if ($entry = ldap_first_entry($this->ldapconnection
, $result)) {
879 $attributes = ldap_get_attributes($this->ldapconnection
, $entry);
880 for ($j = 0; $j < $attributes['count']; $j++
) {
881 $groups = ldap_get_values_len($this->ldapconnection
, $entry, $attributes[$j]);
882 foreach ($groups as $key => $group) {
883 if ($key === 'count') { // Skip the entries count
886 if(!in_array($group, $membergroups)) {
887 // Only push and recurse if we haven't 'seen' this group before
888 // to prevent loops (MS Active Directory allows them!!).
889 array_push($membergroups, $group);
890 $this->ldap_find_user_groups_recursively($group, $membergroups);
895 while ($entry = ldap_next_entry($this->ldapconnection
, $entry));
900 * Given a group name (either a RDN or a DN), get the list of users
901 * belonging to that group. If the group has nested groups, expand all
902 * the intermediate groups and return the full list of users that
903 * directly or indirectly belong to the group.
905 * @param string $group the group name to search
906 * @param string $memberattibute the attribute that holds the members of the group
907 * @return array the list of users belonging to the group. If $group
908 * is not actually a group, returns array($group).
910 protected function ldap_explode_group($group, $memberattribute) {
911 switch ($this->get_config('user_type')) {
913 // $group is already the distinguished name to search.
916 $result = ldap_read($this->ldapconnection
, $dn, '(objectClass=*)', array('objectClass'));
917 $entry = ldap_first_entry($this->ldapconnection
, $result);
918 $objectclass = ldap_get_values($this->ldapconnection
, $entry, 'objectClass');
920 if (!in_array('group', $objectclass)) {
921 // Not a group, so return immediately.
922 return array($group);
925 $result = ldap_read($this->ldapconnection
, $dn, '(objectClass=*)', array($memberattribute));
926 $entry = ldap_first_entry($this->ldapconnection
, $result);
927 $members = @ldap_get_values
($this->ldapconnection
, $entry, $memberattribute); // Can be empty and throws a warning
928 if ($members['count'] == 0) {
929 // There are no members in this group, return nothing.
932 unset($members['count']);
935 foreach ($members as $member) {
936 $group_members = $this->ldap_explode_group($member, $memberattribute);
937 $users = array_merge($users, $group_members);
943 error_log($this->errorlogtag
.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
944 $this->get_config('user_type_name')));
946 return array($group);
951 * Will create the moodle course from the template
952 * course_ext is an array as obtained from ldap -- flattened somewhat
954 * @param array $course_ext
955 * @param progress_trace $trace
956 * @return mixed false on error, id for the newly created course otherwise.
958 function create_course($course_ext, progress_trace
$trace) {
961 require_once("$CFG->dirroot/course/lib.php");
963 // Override defaults with template course
965 if ($this->get_config('template')) {
966 if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
967 $template = fullclone(course_get_format($template)->get_course());
968 unset($template->id
); // So we are clear to reinsert the record
969 unset($template->fullname
);
970 unset($template->shortname
);
971 unset($template->idnumber
);
975 $courseconfig = get_config('moodlecourse');
976 $template = new stdClass();
977 $template->summary
= '';
978 $template->summaryformat
= FORMAT_HTML
;
979 $template->format
= $courseconfig->format
;
980 $template->newsitems
= $courseconfig->newsitems
;
981 $template->showgrades
= $courseconfig->showgrades
;
982 $template->showreports
= $courseconfig->showreports
;
983 $template->maxbytes
= $courseconfig->maxbytes
;
984 $template->groupmode
= $courseconfig->groupmode
;
985 $template->groupmodeforce
= $courseconfig->groupmodeforce
;
986 $template->visible
= $courseconfig->visible
;
987 $template->lang
= $courseconfig->lang
;
988 $template->enablecompletion
= $courseconfig->enablecompletion
;
992 $course->category
= $this->get_config('category');
993 if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
994 $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
995 $first = reset($categories);
996 $course->category
= $first->id
;
999 // Override with required ext data
1000 $course->idnumber
= $course_ext[$this->get_config('course_idnumber')][0];
1001 $course->fullname
= $course_ext[$this->get_config('course_fullname')][0];
1002 $course->shortname
= $course_ext[$this->get_config('course_shortname')][0];
1003 if (empty($course->idnumber
) ||
empty($course->fullname
) ||
empty($course->shortname
)) {
1004 // We are in trouble!
1005 $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
1009 $summary = $this->get_config('course_summary');
1010 if (!isset($summary) ||
empty($course_ext[$summary][0])) {
1011 $course->summary
= '';
1013 $course->summary
= $course_ext[$this->get_config('course_summary')][0];
1016 // Check if the shortname already exists if it does - skip course creation.
1017 if ($DB->record_exists('course', array('shortname' => $course->shortname
))) {
1018 $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));
1022 $newcourse = create_course($course);
1023 return $newcourse->id
;
1027 * Will update a moodle course with new values from LDAP
1028 * A field will be updated only if it is marked to be updated
1029 * on sync in plugin settings
1031 * @param object $course
1032 * @param array $externalcourse
1033 * @param progress_trace $trace
1036 protected function update_course($course, $externalcourse, progress_trace
$trace) {
1039 $coursefields = array ('shortname', 'fullname', 'summary');
1040 static $shouldupdate;
1042 // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update.
1043 if (!isset($shouldupdate)) {
1044 $shouldupdate = false;
1045 foreach ($coursefields as $field) {
1046 $shouldupdate = $shouldupdate ||
$this->get_config('course_'.$field.'_updateonsync');
1050 // If we should not update return immediately.
1051 if (!$shouldupdate) {
1055 require_once("$CFG->dirroot/course/lib.php");
1056 $courseupdated = false;
1057 $updatedcourse = new stdClass();
1058 $updatedcourse->id
= $course->id
;
1060 // Update course fields if necessary.
1061 foreach ($coursefields as $field) {
1062 // If field is marked to be updated on sync && field data was changed update it.
1063 if ($this->get_config('course_'.$field.'_updateonsync')
1064 && isset($externalcourse[$this->get_config('course_'.$field)][0])
1065 && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) {
1066 $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0];
1067 $courseupdated = true;
1071 if (!$courseupdated) {
1072 $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));
1076 // Do not allow empty fullname or shortname.
1077 if ((isset($updatedcourse->fullname
) && empty($updatedcourse->fullname
))
1078 ||
(isset($updatedcourse->shortname
) && empty($updatedcourse->shortname
))) {
1079 // We are in trouble!
1080 $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course));
1084 // Check if the shortname already exists if it does - skip course updating.
1085 if (isset($updatedcourse->shortname
)
1086 && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname
))) {
1087 $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course));
1091 // Finally - update course in DB.
1092 update_course($updatedcourse);
1093 $trace->output(get_string('courseupdated', 'enrol_ldap', $course));
1099 * Automatic enrol sync executed during restore.
1100 * Useful for automatic sync by course->idnumber or course category.
1101 * @param stdClass $course course record
1103 public function restore_sync_course($course) {
1104 // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)
1105 // NOTE: for now restore does not do any real logging yet, let's do the same here...
1106 $trace = new error_log_progress_trace();
1107 $this->sync_enrolments($trace, $course->id
);
1111 * Restore instance and map settings.
1113 * @param restore_enrolments_structure_step $step
1114 * @param stdClass $data
1115 * @param stdClass $course
1118 public function restore_instance(restore_enrolments_structure_step
$step, stdClass
$data, $course, $oldid) {
1120 // There is only 1 ldap enrol instance per course.
1121 if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid
, 'enrol'=>'ldap'), 'id')) {
1122 $instance = reset($instances);
1123 $instanceid = $instance->id
;
1125 $instanceid = $this->add_instance($course, (array)$data);
1127 $step->set_mapping('enrol', $oldid, $instanceid);
1131 * Restore user enrolment.
1133 * @param restore_enrolments_structure_step $step
1134 * @param stdClass $data
1135 * @param stdClass $instance
1136 * @param int $oldinstancestatus
1137 * @param int $userid
1139 public function restore_user_enrolment(restore_enrolments_structure_step
$step, $data, $instance, $userid, $oldinstancestatus) {
1142 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL
) {
1143 // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
1145 } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP
) {
1146 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
1147 $this->enrol_user($instance, $userid, null, 0, 0, $data->status
);
1151 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
1152 $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED
);
1158 * Restore role assignment.
1160 * @param stdClass $instance
1161 * @param int $roleid
1162 * @param int $userid
1163 * @param int $contextid
1165 public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
1168 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL
or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
1169 // Skip any roles restore, they should be already synced automatically.
1173 // Just restore every role.
1174 if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
1175 role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol
, $instance->id
);