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 * The object class to use when finding users.
39 * @var string $userobjectclass
41 protected $userobjectclass;
43 /** @var LDAP\Connection LDAP connection. */
44 protected $ldapconnection;
47 * Constructor for the plugin. In addition to calling the parent
48 * constructor, we define and 'fix' some settings depending on the
49 * real settings the admin defined.
51 public function __construct() {
53 require_once($CFG->libdir
.'/ldaplib.php');
55 // Do our own stuff to fix the config (it's easier to do it
56 // here than using the admin settings infrastructure). We
57 // don't call $this->set_config() for any of the 'fixups'
58 // (except the objectclass, as it's critical) because the user
59 // didn't specify any values and relied on the default values
60 // defined for the user type she chose.
63 // Make sure we get sane defaults for critical values.
64 $this->config
->ldapencoding
= $this->get_config('ldapencoding', 'utf-8');
65 $this->config
->user_type
= $this->get_config('user_type', 'default');
67 $ldap_usertypes = ldap_supported_usertypes();
68 $this->config
->user_type_name
= $ldap_usertypes[$this->config
->user_type
];
69 unset($ldap_usertypes);
71 $default = ldap_getdefaults();
73 // The objectclass in the defaults is for a user.
74 // This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass.
75 // Save the normalised user objectclass for later.
76 $this->userobjectclass
= ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]);
78 // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.
79 unset($default['objectclass']);
81 // Use defaults if values not given. Dont use this->get_config()
82 // here to be able to check for 0 and false values too.
83 foreach ($default as $key => $value) {
84 // Watch out - 0, false are correct values too, so we can't use $this->get_config()
85 if (!isset($this->config
->{$key}) or $this->config
->{$key} == '') {
86 $this->config
->{$key} = $value[$this->config
->user_type
];
90 // Normalise the objectclass used for groups.
91 if (empty($this->config
->objectclass
)) {
92 // No objectclass set yet - set a default class.
93 $this->config
->objectclass
= ldap_normalise_objectclass(null, '*');
94 $this->set_config('objectclass', $this->config
->objectclass
);
96 $objectclass = ldap_normalise_objectclass($this->config
->objectclass
);
97 if ($objectclass !== $this->config
->objectclass
) {
98 // The objectclass was changed during normalisation.
99 // Save it in config, and update the local copy of config.
100 $this->set_config('objectclass', $objectclass);
101 $this->config
->objectclass
= $objectclass;
107 * Is it possible to delete enrol instance via standard UI?
109 * @param object $instance
112 public function can_delete_instance($instance) {
113 $context = context_course
::instance($instance->courseid
);
114 if (!has_capability('enrol/ldap:manage', $context)) {
118 if (!enrol_is_enabled('ldap')) {
122 if (!$this->get_config('ldap_host') or !$this->get_config('objectclass') or !$this->get_config('course_idnumber')) {
126 // TODO: connect to external system and make sure no users are to be enrolled in this course
131 * Is it possible to hide/show enrol instance via standard UI?
133 * @param stdClass $instance
136 public function can_hide_show_instance($instance) {
137 $context = context_course
::instance($instance->courseid
);
138 return has_capability('enrol/ldap:manage', $context);
142 * Forces synchronisation of user enrolments with LDAP server.
143 * It creates courses if the plugin is configured to do so.
145 * @param object $user user record
148 public function sync_user_enrolments($user) {
151 // Do not try to print anything to the output because this method is called during interactive login.
153 $trace = new null_progress_trace();
155 $trace = new error_log_progress_trace($this->errorlogtag
);
158 if (!$this->ldap_connect($trace)) {
163 if (!is_object($user) or !property_exists($user, 'id')) {
164 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
167 if (!property_exists($user, 'idnumber')) {
168 debugging('Invalid $user parameter in sync_user_enrolments(), missing idnumber');
169 $user = $DB->get_record('user', array('id'=>$user->id
));
172 // We may need a lot of memory here
173 core_php_time_limit
::raise();
174 raise_memory_limit(MEMORY_HUGE
);
176 // Get enrolments for each type of role.
177 $roles = get_all_roles();
178 $enrolments = array();
179 foreach($roles as $role) {
180 // Get external enrolments according to LDAP server
181 $enrolments[$role->id
]['ext'] = $this->find_ext_enrolments($user->idnumber
, $role);
183 // Get the list of current user enrolments that come from LDAP
184 $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
186 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
187 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
188 JOIN {enrol} e ON (e.id = ue.enrolid)
189 JOIN {course} c ON (c.id = e.courseid)
190 WHERE u.deleted = 0 AND u.id = :userid";
191 $params = array ('roleid'=>$role->id
, 'userid'=>$user->id
);
192 $enrolments[$role->id
]['current'] = $DB->get_records_sql($sql, $params);
195 $ignorehidden = $this->get_config('ignorehiddencourses');
196 $courseidnumber = $this->get_config('course_idnumber');
197 foreach($roles as $role) {
198 foreach ($enrolments[$role->id
]['ext'] as $enrol) {
199 $course_ext_id = $enrol[$courseidnumber][0];
200 if (empty($course_ext_id)) {
201 $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
202 continue; // Next; skip this one!
205 // Create the course if required
206 $course = $DB->get_record('course', array($this->enrol_localcoursefield
=>$course_ext_id));
207 if (empty($course)) { // Course doesn't exist
208 if ($this->get_config('autocreate')) { // Autocreate
209 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
210 if (!$newcourseid = $this->create_course($enrol, $trace)) {
213 $course = $DB->get_record('course', array('id'=>$newcourseid));
215 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
216 continue; // Next; skip this one!
220 // Deal with enrolment in the moodle db
221 // Add necessary enrol instance if not present yet;
222 $sql = "SELECT c.id, c.visible, e.id as enrolid
224 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
225 WHERE c.id = :courseid";
226 $params = array('courseid'=>$course->id
);
227 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE
))) {
228 $course_instance = new stdClass();
229 $course_instance->id
= $course->id
;
230 $course_instance->visible
= $course->visible
;
231 $course_instance->enrolid
= $this->add_instance($course_instance);
234 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid
))) {
235 continue; // Weird; skip this one.
238 if ($ignorehidden && !$course_instance->visible
) {
242 if (empty($enrolments[$role->id
]['current'][$course->id
])) {
243 // Enrol the user in the given course, with that role.
244 $this->enrol_user($instance, $user->id
, $role->id
);
245 // Make sure we set the enrolment status to active. If the user wasn't
246 // previously enrolled to the course, enrol_user() sets it. But if we
247 // configured the plugin to suspend the user enrolments _AND_ remove
248 // the role assignments on external unenrol, then enrol_user() doesn't
249 // set it back to active on external re-enrolment. So set it
250 // unconditionnally to cover both cases.
251 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
252 $trace->output(get_string('enroluser', 'enrol_ldap',
253 array('user_username'=> $user->username
,
254 'course_shortname'=>$course->shortname
,
255 'course_id'=>$course->id
)));
257 if ($enrolments[$role->id
]['current'][$course->id
]->status
== ENROL_USER_SUSPENDED
) {
258 // Reenable enrolment that was previously disabled. Enrolment refreshed
259 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
260 $trace->output(get_string('enroluserenable', 'enrol_ldap',
261 array('user_username'=> $user->username
,
262 'course_shortname'=>$course->shortname
,
263 'course_id'=>$course->id
)));
267 // Remove this course from the current courses, to be able to detect
268 // which current courses should be unenroled from when we finish processing
269 // external enrolments.
270 unset($enrolments[$role->id
]['current'][$course->id
]);
273 // Deal with unenrolments.
274 $transaction = $DB->start_delegated_transaction();
275 foreach ($enrolments[$role->id
]['current'] as $course) {
276 $context = context_course
::instance($course->courseid
);
277 $instance = $DB->get_record('enrol', array('id'=>$course->enrolid
));
278 switch ($this->get_config('unenrolaction')) {
279 case ENROL_EXT_REMOVED_UNENROL
:
280 $this->unenrol_user($instance, $user->id
);
281 $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
282 array('user_username'=> $user->username
,
283 'course_shortname'=>$course->shortname
,
284 'course_id'=>$course->courseid
)));
286 case ENROL_EXT_REMOVED_KEEP
:
287 // Keep - only adding enrolments
289 case ENROL_EXT_REMOVED_SUSPEND
:
290 if ($course->status
!= ENROL_USER_SUSPENDED
) {
291 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
292 $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
293 array('user_username'=> $user->username
,
294 'course_shortname'=>$course->shortname
,
295 'course_id'=>$course->courseid
)));
298 case ENROL_EXT_REMOVED_SUSPENDNOROLES
:
299 if ($course->status
!= ENROL_USER_SUSPENDED
) {
300 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$user->id
));
302 role_unassign_all(array('contextid'=>$context->id
, 'userid'=>$user->id
, 'component'=>'enrol_ldap', 'itemid'=>$instance->id
));
303 $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
304 array('user_username'=> $user->username
,
305 'course_shortname'=>$course->shortname
,
306 'course_id'=>$course->courseid
)));
310 $transaction->allow_commit();
319 * Forces synchronisation of all enrolments with LDAP server.
320 * It creates courses if the plugin is configured to do so.
322 * @param progress_trace $trace
323 * @param int|null $onecourse limit sync to one course->id, null if all courses
326 public function sync_enrolments(progress_trace
$trace, $onecourse = null) {
329 if (!$this->ldap_connect($trace)) {
334 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection
);
336 // we may need a lot of memory here
337 core_php_time_limit
::raise();
338 raise_memory_limit(MEMORY_HUGE
);
342 if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield
)) {
343 // Course does not exist, nothing to do.
344 $trace->output("Requested course $onecourse does not exist, no sync performed.");
348 if (empty($course->{$this->enrol_localcoursefield
})) {
349 $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");
353 $oneidnumber = ldap_filter_addslashes(core_text
::convert($course->idnumber
, 'utf-8', $this->get_config('ldapencoding')));
356 // Get enrolments for each type of role.
357 $roles = get_all_roles();
358 $enrolments = array();
359 foreach($roles as $role) {
361 $ldap_contexts = explode(';', $this->config
->{'contexts_role'.$role->id
});
363 // Get all the fields we will want for the potential course creation
364 // as they are light. Don't get membership -- potentially a lot of data.
365 $ldap_fields_wanted = array('dn', $this->config
->course_idnumber
);
366 if (!empty($this->config
->course_fullname
)) {
367 array_push($ldap_fields_wanted, $this->config
->course_fullname
);
369 if (!empty($this->config
->course_shortname
)) {
370 array_push($ldap_fields_wanted, $this->config
->course_shortname
);
372 if (!empty($this->config
->course_summary
)) {
373 array_push($ldap_fields_wanted, $this->config
->course_summary
);
375 array_push($ldap_fields_wanted, $this->config
->{'memberattribute_role'.$role->id
});
377 // Define the search pattern
378 $ldap_search_pattern = $this->config
->objectclass
;
380 if ($oneidnumber !== null) {
381 $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
385 $servercontrols = array();
386 foreach ($ldap_contexts as $ldap_context) {
387 $ldap_context = trim($ldap_context);
388 if (empty($ldap_context)) {
392 $flat_records = array();
394 if ($ldap_pagedresults) {
395 $servercontrols = array(array(
396 'oid' => LDAP_CONTROL_PAGEDRESULTS
, 'value' => array(
397 'size' => $this->config
->pagesize
, 'cookie' => $ldap_cookie)));
400 if ($this->config
->course_search_sub
) {
401 // Use ldap_search to find first user from subtree
402 $ldap_result = @ldap_search
($this->ldapconnection
, $ldap_context,
403 $ldap_search_pattern, $ldap_fields_wanted,
404 0, -1, -1, LDAP_DEREF_NEVER
, $servercontrols);
406 // Search only in this context
407 $ldap_result = @ldap_list
($this->ldapconnection
, $ldap_context,
408 $ldap_search_pattern, $ldap_fields_wanted,
409 0, -1, -1, LDAP_DEREF_NEVER
, $servercontrols);
415 if ($ldap_pagedresults) {
416 // Get next server cookie to know if we'll need to continue searching.
418 // Get next cookie from controls.
419 ldap_parse_result($this->ldapconnection
, $ldap_result, $errcode, $matcheddn,
420 $errmsg, $referrals, $controls);
421 if (isset($controls[LDAP_CONTROL_PAGEDRESULTS
]['value']['cookie'])) {
422 $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS
]['value']['cookie'];
426 // Check and push results
427 $records = ldap_get_entries($this->ldapconnection
, $ldap_result);
429 // LDAP libraries return an odd array, really. fix it:
430 for ($c = 0; $c < $records['count']; $c++
) {
431 array_push($flat_records, $records[$c]);
435 } while ($ldap_pagedresults && !empty($ldap_cookie));
437 // If LDAP paged results were used, the current connection must be completely
438 // closed and a new one created, to work without paged results from here on.
439 if ($ldap_pagedresults) {
441 $this->ldap_connect($trace);
444 if (count($flat_records)) {
445 $ignorehidden = $this->get_config('ignorehiddencourses');
446 foreach($flat_records as $course) {
447 $course = array_change_key_case($course, CASE_LOWER
);
448 $idnumber = $course[$this->config
->course_idnumber
][0];
449 $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname
)));
451 // Does the course exist in moodle already?
452 $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield
=>$idnumber));
453 if (empty($course_obj)) { // Course doesn't exist
454 if ($this->get_config('autocreate')) { // Autocreate
455 $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
456 if (!$newcourseid = $this->create_course($course, $trace)) {
459 $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
461 $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
462 continue; // Next; skip this one!
464 } else { // Check if course needs update & update as needed.
465 $this->update_course($course_obj, $course, $trace);
470 // Pull the ldap membership into a nice array
471 // this is an odd array -- mix of hash and array --
472 $ldapmembers = array();
474 if (property_exists($this->config
, 'memberattribute_role'.$role->id
)
475 && !empty($this->config
->{'memberattribute_role'.$role->id
})
476 && !empty($course[$this->config
->{'memberattribute_role'.$role->id
}])) { // May have no membership!
478 $ldapmembers = $course[$this->config
->{'memberattribute_role'.$role->id
}];
479 unset($ldapmembers['count']); // Remove oddity ;)
481 // If we have enabled nested groups, we need to expand
482 // the groups to get the real user list. We need to do
483 // this before dealing with 'memberattribute_isdn'.
484 if ($this->config
->nested_groups
) {
486 foreach ($ldapmembers as $ldapmember) {
487 $grpusers = $this->ldap_explode_group($ldapmember,
488 $this->config
->{'memberattribute_role'.$role->id
});
490 $users = array_merge($users, $grpusers);
492 $ldapmembers = array_unique($users); // There might be duplicates.
495 // Deal with the case where the member attribute holds distinguished names,
496 // but only if the user attribute is not a distinguished name itself.
497 if ($this->config
->memberattribute_isdn
498 && ($this->config
->idnumber_attribute
!== 'dn')
499 && ($this->config
->idnumber_attribute
!== 'distinguishedname')) {
500 // We need to retrieve the idnumber for all the users in $ldapmembers,
501 // as the idnumber does not match their dn and we get dn's from membership.
502 $memberidnumbers = array();
503 foreach ($ldapmembers as $ldapmember) {
504 $result = ldap_read($this->ldapconnection
, $ldapmember, $this->userobjectclass
,
505 array($this->config
->idnumber_attribute
));
506 $entry = ldap_first_entry($this->ldapconnection
, $result);
507 $values = ldap_get_values($this->ldapconnection
, $entry, $this->config
->idnumber_attribute
);
508 array_push($memberidnumbers, $values[0]);
511 $ldapmembers = $memberidnumbers;
515 // Prune old ldap enrolments
516 // hopefully they'll fit in the max buffer size for the RDBMS
517 $sql= "SELECT u.id as userid, u.username, ue.status,
518 ra.contextid, ra.itemid as instanceid
520 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_ldap' AND ra.roleid = :roleid)
521 JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
522 JOIN {enrol} e ON (e.id = ue.enrolid)
523 WHERE u.deleted = 0 AND e.courseid = :courseid ";
524 $params = array('roleid'=>$role->id
, 'courseid'=>$course_obj->id
);
525 $context = context_course
::instance($course_obj->id
);
526 if (!empty($ldapmembers)) {
527 list($ldapml, $params2) = $DB->get_in_or_equal($ldapmembers, SQL_PARAMS_NAMED
, 'm', false);
528 $sql .= "AND u.idnumber $ldapml";
529 $params = array_merge($params, $params2);
532 $shortname = format_string($course_obj->shortname
, true, array('context' => $context));
533 $trace->output(get_string('emptyenrolment', 'enrol_ldap',
534 array('role_shortname'=> $role->shortname
,
535 'course_shortname' => $shortname)));
537 $todelete = $DB->get_records_sql($sql, $params);
539 if (!empty($todelete)) {
540 $transaction = $DB->start_delegated_transaction();
541 foreach ($todelete as $row) {
542 $instance = $DB->get_record('enrol', array('id'=>$row->instanceid
));
543 switch ($this->get_config('unenrolaction')) {
544 case ENROL_EXT_REMOVED_UNENROL
:
545 $this->unenrol_user($instance, $row->userid
);
546 $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
547 array('user_username'=> $row->username
,
548 'course_shortname'=>$course_obj->shortname
,
549 'course_id'=>$course_obj->id
)));
551 case ENROL_EXT_REMOVED_KEEP
:
552 // Keep - only adding enrolments
554 case ENROL_EXT_REMOVED_SUSPEND
:
555 if ($row->status
!= ENROL_USER_SUSPENDED
) {
556 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$row->userid
));
557 $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
558 array('user_username'=> $row->username
,
559 'course_shortname'=>$course_obj->shortname
,
560 'course_id'=>$course_obj->id
)));
563 case ENROL_EXT_REMOVED_SUSPENDNOROLES
:
564 if ($row->status
!= ENROL_USER_SUSPENDED
) {
565 $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED
, array('enrolid'=>$instance->id
, 'userid'=>$row->userid
));
567 role_unassign_all(array('contextid'=>$row->contextid
, 'userid'=>$row->userid
, 'component'=>'enrol_ldap', 'itemid'=>$instance->id
));
568 $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
569 array('user_username'=> $row->username
,
570 'course_shortname'=>$course_obj->shortname
,
571 'course_id'=>$course_obj->id
)));
575 $transaction->allow_commit();
578 // Insert current enrolments
579 // bad we can't do INSERT IGNORE with postgres...
581 // Add necessary enrol instance if not present yet;
582 $sql = "SELECT c.id, c.visible, e.id as enrolid
584 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'ldap')
585 WHERE c.id = :courseid";
586 $params = array('courseid'=>$course_obj->id
);
587 if (!($course_instance = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE
))) {
588 $course_instance = new stdClass();
589 $course_instance->id
= $course_obj->id
;
590 $course_instance->visible
= $course_obj->visible
;
591 $course_instance->enrolid
= $this->add_instance($course_instance);
594 if (!$instance = $DB->get_record('enrol', array('id'=>$course_instance->enrolid
))) {
595 continue; // Weird; skip this one.
598 if ($ignorehidden && !$course_instance->visible
) {
602 $transaction = $DB->start_delegated_transaction();
603 foreach ($ldapmembers as $ldapmember) {
604 $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
605 $member = $DB->get_record_sql($sql, array($ldapmember));
606 if(empty($member) ||
empty($member->id
)){
607 $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
611 $sql= "SELECT ue.status
612 FROM {user_enrolments} ue
613 JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
614 WHERE e.courseid = :courseid AND ue.userid = :userid";
615 $params = array('courseid'=>$course_obj->id
, 'userid'=>$member->id
);
616 $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE
);
618 if (empty($userenrolment)) {
619 $this->enrol_user($instance, $member->id
, $role->id
);
620 // Make sure we set the enrolment status to active. If the user wasn't
621 // previously enrolled to the course, enrol_user() sets it. But if we
622 // configured the plugin to suspend the user enrolments _AND_ remove
623 // the role assignments on external unenrol, then enrol_user() doesn't
624 // set it back to active on external re-enrolment. So set it
625 // unconditionally to cover both cases.
626 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$member->id
));
627 $trace->output(get_string('enroluser', 'enrol_ldap',
628 array('user_username'=> $member->username
,
629 'course_shortname'=>$course_obj->shortname
,
630 'course_id'=>$course_obj->id
)));
633 if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id
, 'userid'=>$member->id
, 'contextid'=>$context->id
, 'component'=>'enrol_ldap', 'itemid'=>$instance->id
))) {
634 // This happens when reviving users or when user has multiple roles in one course.
635 $context = context_course
::instance($course_obj->id
);
636 role_assign($role->id
, $member->id
, $context->id
, 'enrol_ldap', $instance->id
);
637 $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
639 if ($userenrolment->status
== ENROL_USER_SUSPENDED
) {
640 // Reenable enrolment that was previously disabled. Enrolment refreshed
641 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE
, array('enrolid'=>$instance->id
, 'userid'=>$member->id
));
642 $trace->output(get_string('enroluserenable', 'enrol_ldap',
643 array('user_username'=> $member->username
,
644 'course_shortname'=>$course_obj->shortname
,
645 'course_id'=>$course_obj->id
)));
649 $transaction->allow_commit();
654 @$this->ldap_close();
659 * Connect to the LDAP server, using the plugin configured
660 * settings. It's actually a wrapper around ldap_connect_moodle()
662 * @param progress_trace $trace
663 * @return bool success
665 protected function ldap_connect(progress_trace
$trace = null) {
667 require_once($CFG->libdir
.'/ldaplib.php');
669 if (isset($this->ldapconnection
)) {
673 if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
674 $this->get_config('user_type'), $this->get_config('bind_dn'),
675 $this->get_config('bind_pw'), $this->get_config('opt_deref'),
676 $debuginfo, $this->get_config('start_tls'))) {
677 $this->ldapconnection
= $ldapconnection;
682 $trace->output($debuginfo);
684 error_log($this->errorlogtag
.$debuginfo);
691 * Disconnects from a LDAP server
694 protected function ldap_close() {
695 if (isset($this->ldapconnection
)) {
696 @ldap_close
($this->ldapconnection
);
697 $this->ldapconnection
= null;
703 * Return multidimensional array with details of user courses (at
704 * least dn and idnumber).
706 * @param string $memberuid user idnumber (without magic quotes).
707 * @param object role is a record from the mdl_role table.
710 protected function find_ext_enrolments($memberuid, $role) {
712 require_once($CFG->libdir
.'/ldaplib.php');
714 if (empty($memberuid)) {
715 // No "idnumber" stored for this user, so no LDAP enrolments
719 $ldap_contexts = trim($this->get_config('contexts_role'.$role->id
));
720 if (empty($ldap_contexts)) {
721 // No role contexts, so no LDAP enrolments
725 $extmemberuid = core_text
::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
727 if($this->get_config('memberattribute_isdn')) {
728 if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
733 $ldap_search_pattern = '';
734 if($this->get_config('nested_groups')) {
735 $usergroups = $this->ldap_find_user_groups($extmemberuid);
736 if(count($usergroups) > 0) {
737 foreach ($usergroups as $group) {
738 $group = ldap_filter_addslashes($group);
739 $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id
).'='.$group.')';
744 // Default return value
747 // Get all the fields we will want for the potential course creation
748 // as they are light. don't get membership -- potentially a lot of data.
749 $ldap_fields_wanted = array('dn', $this->get_config('course_idnumber'));
750 $fullname = $this->get_config('course_fullname');
751 $shortname = $this->get_config('course_shortname');
752 $summary = $this->get_config('course_summary');
753 if (isset($fullname)) {
754 array_push($ldap_fields_wanted, $fullname);
756 if (isset($shortname)) {
757 array_push($ldap_fields_wanted, $shortname);
759 if (isset($summary)) {
760 array_push($ldap_fields_wanted, $summary);
763 // Define the search pattern
764 if (empty($ldap_search_pattern)) {
765 $ldap_search_pattern = '('.$this->get_config('memberattribute_role'.$role->id
).'='.ldap_filter_addslashes($extmemberuid).')';
767 $ldap_search_pattern = '(|' . $ldap_search_pattern .
768 '('.$this->get_config('memberattribute_role'.$role->id
).'='.ldap_filter_addslashes($extmemberuid).')' .
771 $ldap_search_pattern='(&'.$this->get_config('objectclass').$ldap_search_pattern.')';
773 // Get all contexts and look for first matching user
774 $ldap_contexts = explode(';', $ldap_contexts);
775 $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection
);
776 foreach ($ldap_contexts as $context) {
777 $context = trim($context);
778 if (empty($context)) {
783 $servercontrols = array();
784 $flat_records = array();
786 if ($ldap_pagedresults) {
787 $servercontrols = array(array(
788 'oid' => LDAP_CONTROL_PAGEDRESULTS
, 'value' => array(
789 'size' => $this->config
->pagesize
, 'cookie' => $ldap_cookie)));
792 if ($this->get_config('course_search_sub')) {
793 // Use ldap_search to find first user from subtree
794 $ldap_result = @ldap_search
($this->ldapconnection
, $context,
795 $ldap_search_pattern, $ldap_fields_wanted,
796 0, -1, -1, LDAP_DEREF_NEVER
, $servercontrols);
798 // Search only in this context
799 $ldap_result = @ldap_list
($this->ldapconnection
, $context,
800 $ldap_search_pattern, $ldap_fields_wanted,
801 0, -1, -1, LDAP_DEREF_NEVER
, $servercontrols);
808 if ($ldap_pagedresults) {
809 // Get next server cookie to know if we'll need to continue searching.
811 // Get next cookie from controls.
812 ldap_parse_result($this->ldapconnection
, $ldap_result, $errcode, $matcheddn,
813 $errmsg, $referrals, $controls);
814 if (isset($controls[LDAP_CONTROL_PAGEDRESULTS
]['value']['cookie'])) {
815 $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS
]['value']['cookie'];
819 // Check and push results. ldap_get_entries() already
820 // lowercases the attribute index, so there's no need to
821 // use array_change_key_case() later.
822 $records = ldap_get_entries($this->ldapconnection
, $ldap_result);
824 // LDAP libraries return an odd array, really. Fix it.
825 for ($c = 0; $c < $records['count']; $c++
) {
826 array_push($flat_records, $records[$c]);
830 } while ($ldap_pagedresults && !empty($ldap_cookie));
832 // If LDAP paged results were used, the current connection must be completely
833 // closed and a new one created, to work without paged results from here on.
834 if ($ldap_pagedresults) {
836 $this->ldap_connect();
839 if (count($flat_records)) {
840 $courses = array_merge($courses, $flat_records);
848 * Search specified contexts for the specified userid and return the
849 * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
850 * around ldap_find_userdn().
852 * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
853 * @return mixed the user dn or false
855 protected function ldap_find_userdn($userid) {
857 require_once($CFG->libdir
.'/ldaplib.php');
859 $ldap_contexts = explode(';', $this->get_config('user_contexts'));
861 return ldap_find_userdn($this->ldapconnection
, $userid, $ldap_contexts,
862 $this->userobjectclass
,
863 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
867 * Find the groups a given distinguished name belongs to, both directly
868 * and indirectly via nested groups membership.
870 * @param string $memberdn distinguished name to search
871 * @return array with member groups' distinguished names (can be emtpy)
873 protected function ldap_find_user_groups($memberdn) {
876 $this->ldap_find_user_groups_recursively($memberdn, $groups);
881 * Recursively process the groups the given member distinguished name
882 * belongs to, adding them to the already processed groups array.
884 * @param string $memberdn distinguished name to search
885 * @param array reference &$membergroups array with already found
886 * groups, where we'll put the newly found
889 protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
890 $result = @ldap_read
($this->ldapconnection
, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
895 if ($entry = ldap_first_entry($this->ldapconnection
, $result)) {
897 $attributes = ldap_get_attributes($this->ldapconnection
, $entry);
898 for ($j = 0; $j < $attributes['count']; $j++
) {
899 $groups = ldap_get_values_len($this->ldapconnection
, $entry, $attributes[$j]);
900 foreach ($groups as $key => $group) {
901 if ($key === 'count') { // Skip the entries count
904 if(!in_array($group, $membergroups)) {
905 // Only push and recurse if we haven't 'seen' this group before
906 // to prevent loops (MS Active Directory allows them!!).
907 array_push($membergroups, $group);
908 $this->ldap_find_user_groups_recursively($group, $membergroups);
913 while ($entry = ldap_next_entry($this->ldapconnection
, $entry));
918 * Given a group name (either a RDN or a DN), get the list of users
919 * belonging to that group. If the group has nested groups, expand all
920 * the intermediate groups and return the full list of users that
921 * directly or indirectly belong to the group.
923 * @param string $group the group name to search
924 * @param string $memberattibute the attribute that holds the members of the group
925 * @return array the list of users belonging to the group. If $group
926 * is not actually a group, returns array($group).
928 protected function ldap_explode_group($group, $memberattribute) {
929 switch ($this->get_config('user_type')) {
931 // $group is already the distinguished name to search.
934 $result = ldap_read($this->ldapconnection
, $dn, '(objectClass=*)', array('objectClass'));
935 $entry = ldap_first_entry($this->ldapconnection
, $result);
936 $objectclass = ldap_get_values($this->ldapconnection
, $entry, 'objectClass');
938 if (!in_array('group', $objectclass)) {
939 // Not a group, so return immediately.
940 return array($group);
943 $result = ldap_read($this->ldapconnection
, $dn, '(objectClass=*)', array($memberattribute));
944 $entry = ldap_first_entry($this->ldapconnection
, $result);
945 $members = @ldap_get_values
($this->ldapconnection
, $entry, $memberattribute); // Can be empty and throws a warning
946 if ($members['count'] == 0) {
947 // There are no members in this group, return nothing.
950 unset($members['count']);
953 foreach ($members as $member) {
954 $group_members = $this->ldap_explode_group($member, $memberattribute);
955 $users = array_merge($users, $group_members);
961 error_log($this->errorlogtag
.get_string('explodegroupusertypenotsupported', 'enrol_ldap',
962 $this->get_config('user_type_name')));
964 return array($group);
969 * Will create the moodle course from the template
970 * course_ext is an array as obtained from ldap -- flattened somewhat
972 * @param array $course_ext
973 * @param progress_trace $trace
974 * @return mixed false on error, id for the newly created course otherwise.
976 function create_course($course_ext, progress_trace
$trace) {
979 require_once("$CFG->dirroot/course/lib.php");
981 // Override defaults with template course
983 if ($this->get_config('template')) {
984 if ($template = $DB->get_record('course', array('shortname'=>$this->get_config('template')))) {
985 $template = fullclone(course_get_format($template)->get_course());
986 unset($template->id
); // So we are clear to reinsert the record
987 unset($template->fullname
);
988 unset($template->shortname
);
989 unset($template->idnumber
);
993 $courseconfig = get_config('moodlecourse');
994 $template = new stdClass();
995 $template->summary
= '';
996 $template->summaryformat
= FORMAT_HTML
;
997 $template->format
= $courseconfig->format
;
998 $template->newsitems
= $courseconfig->newsitems
;
999 $template->showgrades
= $courseconfig->showgrades
;
1000 $template->showreports
= $courseconfig->showreports
;
1001 $template->maxbytes
= $courseconfig->maxbytes
;
1002 $template->groupmode
= $courseconfig->groupmode
;
1003 $template->groupmodeforce
= $courseconfig->groupmodeforce
;
1004 $template->visible
= $courseconfig->visible
;
1005 $template->lang
= $courseconfig->lang
;
1006 $template->enablecompletion
= $courseconfig->enablecompletion
;
1008 $course = $template;
1010 $course->category
= $this->get_config('category');
1011 if (!$DB->record_exists('course_categories', array('id'=>$this->get_config('category')))) {
1012 $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
1013 $first = reset($categories);
1014 $course->category
= $first->id
;
1017 // Override with required ext data
1018 $course->idnumber
= $course_ext[$this->get_config('course_idnumber')][0];
1019 $course->fullname
= $course_ext[$this->get_config('course_fullname')][0];
1020 $course->shortname
= $course_ext[$this->get_config('course_shortname')][0];
1021 if (empty($course->idnumber
) ||
empty($course->fullname
) ||
empty($course->shortname
)) {
1022 // We are in trouble!
1023 $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
1027 $summary = $this->get_config('course_summary');
1028 if (!isset($summary) ||
empty($course_ext[$summary][0])) {
1029 $course->summary
= '';
1031 $course->summary
= $course_ext[$this->get_config('course_summary')][0];
1034 // Check if the shortname already exists if it does - skip course creation.
1035 if ($DB->record_exists('course', array('shortname' => $course->shortname
))) {
1036 $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));
1040 $newcourse = create_course($course);
1041 return $newcourse->id
;
1045 * Will update a moodle course with new values from LDAP
1046 * A field will be updated only if it is marked to be updated
1047 * on sync in plugin settings
1049 * @param object $course
1050 * @param array $externalcourse
1051 * @param progress_trace $trace
1054 protected function update_course($course, $externalcourse, progress_trace
$trace) {
1057 $coursefields = array ('shortname', 'fullname', 'summary');
1058 static $shouldupdate;
1060 // Initialize $shouldupdate variable. Set to true if one or more fields are marked for update.
1061 if (!isset($shouldupdate)) {
1062 $shouldupdate = false;
1063 foreach ($coursefields as $field) {
1064 $shouldupdate = $shouldupdate ||
$this->get_config('course_'.$field.'_updateonsync');
1068 // If we should not update return immediately.
1069 if (!$shouldupdate) {
1073 require_once("$CFG->dirroot/course/lib.php");
1074 $courseupdated = false;
1075 $updatedcourse = new stdClass();
1076 $updatedcourse->id
= $course->id
;
1078 // Update course fields if necessary.
1079 foreach ($coursefields as $field) {
1080 // If field is marked to be updated on sync && field data was changed update it.
1081 if ($this->get_config('course_'.$field.'_updateonsync')
1082 && isset($externalcourse[$this->get_config('course_'.$field)][0])
1083 && $course->{$field} != $externalcourse[$this->get_config('course_'.$field)][0]) {
1084 $updatedcourse->{$field} = $externalcourse[$this->get_config('course_'.$field)][0];
1085 $courseupdated = true;
1089 if (!$courseupdated) {
1090 $trace->output(get_string('courseupdateskipped', 'enrol_ldap', $course));
1094 // Do not allow empty fullname or shortname.
1095 if ((isset($updatedcourse->fullname
) && empty($updatedcourse->fullname
))
1096 ||
(isset($updatedcourse->shortname
) && empty($updatedcourse->shortname
))) {
1097 // We are in trouble!
1098 $trace->output(get_string('cannotupdatecourse', 'enrol_ldap', $course));
1102 // Check if the shortname already exists if it does - skip course updating.
1103 if (isset($updatedcourse->shortname
)
1104 && $DB->record_exists('course', array('shortname' => $updatedcourse->shortname
))) {
1105 $trace->output(get_string('cannotupdatecourse_duplicateshortname', 'enrol_ldap', $course));
1109 // Finally - update course in DB.
1110 update_course($updatedcourse);
1111 $trace->output(get_string('courseupdated', 'enrol_ldap', $course));
1117 * Automatic enrol sync executed during restore.
1118 * Useful for automatic sync by course->idnumber or course category.
1119 * @param stdClass $course course record
1121 public function restore_sync_course($course) {
1122 // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)
1123 // NOTE: for now restore does not do any real logging yet, let's do the same here...
1124 $trace = new error_log_progress_trace();
1125 $this->sync_enrolments($trace, $course->id
);
1129 * Restore instance and map settings.
1131 * @param restore_enrolments_structure_step $step
1132 * @param stdClass $data
1133 * @param stdClass $course
1136 public function restore_instance(restore_enrolments_structure_step
$step, stdClass
$data, $course, $oldid) {
1138 // There is only 1 ldap enrol instance per course.
1139 if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid
, 'enrol'=>'ldap'), 'id')) {
1140 $instance = reset($instances);
1141 $instanceid = $instance->id
;
1143 $instanceid = $this->add_instance($course, (array)$data);
1145 $step->set_mapping('enrol', $oldid, $instanceid);
1149 * Restore user enrolment.
1151 * @param restore_enrolments_structure_step $step
1152 * @param stdClass $data
1153 * @param stdClass $instance
1154 * @param int $oldinstancestatus
1155 * @param int $userid
1157 public function restore_user_enrolment(restore_enrolments_structure_step
$step, $data, $instance, $userid, $oldinstancestatus) {
1160 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL
) {
1161 // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
1163 } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP
) {
1164 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
1165 $this->enrol_user($instance, $userid, null, 0, 0, $data->status
);
1169 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
1170 $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED
);
1176 * Restore role assignment.
1178 * @param stdClass $instance
1179 * @param int $roleid
1180 * @param int $userid
1181 * @param int $contextid
1183 public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
1186 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL
or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
1187 // Skip any roles restore, they should be already synced automatically.
1191 // Just restore every role.
1192 if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
1193 role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol
, $instance->id
);