4 * @author Martin Dougiamas
5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
6 * @package moodle multiauth
8 * Authentication Plugin: LDAP Authentication
10 * Authentication using LDAP (Lightweight Directory Access Protocol).
12 * 2006-08-28 File created.
15 if (!defined('MOODLE_INTERNAL')) {
16 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
20 * LDAP authentication plugin.
22 class auth_plugin_ldap
{
25 * The configuration details for the plugin.
30 * Constructor with initialisation.
32 function auth_plugin_ldap() {
33 $this->config
= get_config('auth/ldap');
34 if (empty($this->config
->ldapencoding
)) {
35 $this->config
->ldapencoding
= 'utf-8';
37 if (empty($this->config
->user_type
)) {
38 $this->config
->user_type
= 'default';
41 $default = $this->ldap_getdefaults();
43 //use defaults if values not given
44 foreach ($default as $key => $value) {
45 // watch out - 0, false are correct values too
46 if (!isset($this->config
->{$key}) or $this->config
->{$key} == '') {
47 $this->config
->{$key} = $value[$this->config
->user_type
];
50 //hack prefix to objectclass
51 if (empty($this->config
->objectclass
)) { // Can't send empty filter
52 $this->config
->objectclass
='objectClass=*';
53 } else if (strpos($this->config
->objectclass
, 'objectClass=') !== 0) {
54 $this->config
->objectclass
= 'objectClass='.$this->config
->objectclass
;
60 * Returns true if the username and password work and false if they are
61 * wrong or don't exist.
63 * @param string $username The username (with system magic quotes)
64 * @param string $password The password (with system magic quotes)
66 * @return bool Authentication success or failure.
68 function user_login($username, $password) {
69 if (! function_exists('ldap_bind')) {
70 print_error('auth_ldapnotinstalled','auth');
74 if (!$username or !$password) { // Don't allow blank usernames or passwords
78 $textlib = textlib_get_instance();
79 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config
->ldapencoding
);
80 $extpassword = $textlib->convert(stripslashes($password), 'utf-8', $this->config
->ldapencoding
);
82 $ldapconnection = $this->ldap_connect();
84 if ($ldapconnection) {
85 $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
87 //if ldap_user_dn is empty, user does not exist
89 ldap_close($ldapconnection);
93 // Try to bind with current username and password
94 $ldap_login = @ldap_bind
($ldapconnection, $ldap_user_dn, $extpassword);
95 ldap_close($ldapconnection);
101 @ldap_close
($ldapconnection);
102 print_error('auth_ldap_noconnect','auth',$this->config
->host_url
);
108 * reads userinformation from ldap and return it in array()
110 * Read user information from external database and returns it as array().
111 * Function should return all information available. If you are saving
112 * this information to moodle user-table you should honor syncronization flags
114 * @param string $username username (with system magic quotes)
116 * @return mixed array with no magic quotes or false on error
118 function get_userinfo($username) {
119 $textlib = textlib_get_instance();
120 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config
->ldapencoding
);
122 $ldapconnection = $this->ldap_connect();
123 $attrmap = $this->ldap_attributes();
126 $search_attribs = array();
128 foreach ($attrmap as $key=>$values) {
129 if (!is_array($values)) {
130 $values = array($values);
132 foreach ($values as $value) {
133 if (!in_array($value, $search_attribs)) {
134 array_push($search_attribs, $value);
139 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
141 if (!$user_info_result = ldap_read($ldapconnection, $user_dn, $this->config
->objectclass
, $search_attribs)) {
142 return false; // error!
144 $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
145 if (empty($user_entry)) {
146 return false; // entry not found
149 foreach ($attrmap as $key=>$values) {
150 if (!is_array($values)) {
151 $values = array($values);
154 foreach ($values as $value) {
155 if (!array_key_exists($value, $user_entry[0])) {
156 continue; // wrong data mapping!
158 if (is_array($user_entry[0][$value])) {
159 $newval = $textlib->convert($user_entry[0][$value][0], $this->config
->ldapencoding
, 'utf-8');
161 $newval = $textlib->convert($user_entry[0][$value], $this->config
->ldapencoding
, 'utf-8');
163 if (!empty($newval)) { // favour ldap entries that are set
167 if (!is_null($ldapval)) {
168 $result[$key] = $ldapval;
172 @ldap_close
($ldapconnection);
177 * reads userinformation from ldap and return it in an object
179 * @param string $username username (with system magic quotes)
180 * @return mixed object or false on error
182 function get_userinfo_asobj($username) {
183 $user_array = $this->get_userinfo($username);
184 if ($user_array == false) {
185 return false; //error or not found
187 $user_array = truncate_userinfo($user_array);
188 $user = new object();
189 foreach ($user_array as $key=>$value) {
190 $user->{$key} = $value;
196 * returns all usernames from external database
198 * get_userlist returns all usernames from external database
202 function get_userlist() {
203 return $this->ldap_get_userlist("({$this->config->user_attribute}=*)");
207 * checks if user exists on external db
209 * @param string $username (with system magic quotes)
211 function user_exists($username) {
213 $textlib = textlib_get_instance();
214 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config
->ldapencoding
);
216 //returns true if given username exist on ldap
217 $users = $this->ldap_get_userlist("({$this->config->user_attribute}=".$this->filter_addslashes($extusername).")");
218 return count($users);
222 * Creates a new user on external database.
223 * By using information in userobject
224 * Use user_exists to prevent dublicate usernames
226 * @param mixed $userobject Moodle userobject (with system magic quotes)
227 * @param mixed $plainpass Plaintext password (with system magic quotes)
229 function user_create($userobject, $plainpass) {
230 $textlib = textlib_get_instance();
231 $extusername = $textlib->convert(stripslashes($userobject->username
), 'utf-8', $this->config
->ldapencoding
);
232 $extpassword = $textlib->convert(stripslashes($plainpass), 'utf-8', $this->config
->ldapencoding
);
234 $ldapconnection = $this->ldap_connect();
235 $attrmap = $this->ldap_attributes();
239 foreach ($attrmap as $key => $values) {
240 if (!is_array($values)) {
241 $values = array($values);
243 foreach ($values as $value) {
244 if (!empty($userobject->$key) ) {
245 $newuser[$value] = $textlib->convert(stripslashes($userobject->$key), 'utf-8', $this->config
->ldapencoding
);
250 //Following sets all mandatory and other forced attribute values
251 //User should be creted as login disabled untill email confirmation is processed
252 //Feel free to add your user type and send patches to paca@sci.fi to add them
253 //Moodle distribution
255 switch ($this->config
->user_type
) {
257 $newuser['objectClass'] = array("inetOrgPerson","organizationalPerson","person","top");
258 $newuser['uniqueId'] = $extusername;
259 $newuser['logindisabled'] = "TRUE";
260 $newuser['userpassword'] = $extpassword;
263 print_error('auth_ldap_unsupportedusertype','auth',$this->config
->user_type
);
265 $uadd = $this->ldap_add($ldapconnection, $this->config
->user_attribute
.'="'.$this->ldap_addslashes($userobject->username
).','.$this->config
->create_context
.'"', $newuser);
266 ldap_close($ldapconnection);
272 * return number of days to user password expires
274 * If userpassword does not expire it should return 0. If password is already expired
275 * it should return negative value.
277 * @param mixed $username username
280 function password_expire($username) {
283 $textlib = textlib_get_instance();
284 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config
->ldapencoding
);
286 $ldapconnection = $this->ldap_connect();
287 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
288 $search_attribs = array($this->config
->expireattr
);
289 $sr = ldap_read($ldapconnection, $user_dn, 'objectclass=*', $search_attribs);
291 $info = $this->ldap_get_entries($ldapconnection, $sr);
292 if (empty ($info) or empty($info[0][$this->config
->expireattr
][0])) {
293 //error_log("ldap: no expiration value".$info[0][$this->config->expireattr]);
294 // no expiration attribute, password does not expire
299 $expiretime = $this->ldap_expirationtime2unix($info[0][$this->config
->expireattr
][0]);
300 if ($expiretime > $now) {
301 $result = ceil(($expiretime - $now) / DAYSECS
);
304 $result = floor(($expiretime - $now) / DAYSECS
);
308 error_log("ldap: password_expire did't find expiration time.");
311 //error_log("ldap: password_expire user $user_dn expires in $result days!");
316 * syncronizes user fron external db to moodle user table
318 * Sync is now using username attribute.
320 * Syncing users removes or suspends users that dont exists anymore in external db.
321 * Creates new users and updates coursecreator status of users.
323 * @param int $bulk_insert_records will insert $bulkinsert_records per insert statement
324 * valid only with $unsafe. increase to a couple thousand for
325 * blinding fast inserts -- but test it: you may hit mysqld's
326 * max_allowed_packet limit.
327 * @param bool $do_updates will do pull in data updates from ldap if relevant
329 function sync_users ($bulk_insert_records = 1000, $do_updates = true) {
333 $textlib = textlib_get_instance();
335 $droptablesql = array(); /// sql commands to drop the table (because session scope could be a problem for
336 /// some persistent drivers like ODBTP (mssql) or if this function is invoked
337 /// from within a PHP application using persistent connections
339 // configure a temp table
340 print "Configuring temp table\n";
341 switch (strtolower($CFG->dbfamily
)) {
343 $temptable = $CFG->prefix
. 'extuser';
344 $droptablesql[] = 'DROP TEMPORARY TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
345 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
346 echo "Creating temp table $temptable\n";
347 execute_sql('CREATE TEMPORARY TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username)) TYPE=MyISAM', false);
350 $temptable = $CFG->prefix
. 'extuser';
351 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
352 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
353 echo "Creating temp table $temptable\n";
354 $bulk_insert_records = 1; // no support for multiple sets of values
355 execute_sql('CREATE TEMPORARY TABLE '. $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))', false);
358 $temptable = '#'.$CFG->prefix
. 'extuser'; /// MSSQL temp tables begin with #
359 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
360 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
361 echo "Creating temp table $temptable\n";
362 $bulk_insert_records = 1; // no support for multiple sets of values
363 execute_sql('CREATE TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))', false);
366 $temptable = $CFG->prefix
. 'extuser';
367 $droptablesql[] = 'TRUNCATE TABLE ' . $temptable; // oracle requires truncate before being able to drop a temp table
368 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
369 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
370 echo "Creating temp table $temptable\n";
371 $bulk_insert_records = 1; // no support for multiple sets of values
372 execute_sql('CREATE GLOBAL TEMPORARY TABLE '.$temptable.' (username VARCHAR(64), PRIMARY KEY (username)) ON COMMIT PRESERVE ROWS', false);
376 print "Connecting to ldap...\n";
377 $ldapconnection = $this->ldap_connect();
379 if (!$ldapconnection) {
380 @ldap_close
($ldapconnection);
381 print get_string('auth_ldap_noconnect','auth',$this->config
->host_url
);
386 //// get user's list from ldap to sql in a scalable fashion
388 // prepare some data we'll need
389 $filter = "(&(".$this->config
->user_attribute
."=*)(".$this->config
->objectclass
."))";
391 $contexts = explode(";",$this->config
->contexts
);
393 if (!empty($this->config
->create_context
)) {
394 array_push($contexts, $this->config
->create_context
);
398 foreach ($contexts as $context) {
399 $context = trim($context);
400 if (empty($context)) {
404 if ($this->config
->search_sub
) {
405 //use ldap_search to find first user from subtree
406 $ldap_result = ldap_search($ldapconnection, $context,
408 array($this->config
->user_attribute
));
410 //search only in this context
411 $ldap_result = ldap_list($ldapconnection, $context,
413 array($this->config
->user_attribute
));
416 if ($entry = ldap_first_entry($ldapconnection, $ldap_result)) {
418 $value = ldap_get_values_len($ldapconnection, $entry, $this->config
->user_attribute
);
419 $value = $textlib->convert($value[0], $this->config
->ldapencoding
, 'utf-8');
420 array_push($fresult, $value);
421 if (count($fresult) >= $bulk_insert_records) {
422 $this->ldap_bulk_insert($fresult, $temptable);
425 } while ($entry = ldap_next_entry($ldapconnection, $entry));
427 unset($ldap_result); // free mem
429 // insert any remaining users and release mem
430 if (count($fresult)) {
431 $this->ldap_bulk_insert($fresult, $temptable);
437 /// preserve our user database
438 /// if the temp table is empty, it probably means that something went wrong, exit
439 /// so as to avoid mass deletion of users; which is hard to undo
440 $count = get_record_sql('SELECT COUNT(username) AS count, 1 FROM ' . $temptable);
441 $count = $count->{'count'};
443 print "Did not get any users from LDAP -- error? -- exiting\n";
446 print "Got $count records from LDAP\n\n";
451 // find users in DB that aren't in ldap -- to be removed!
452 // this is still not as scalable (but how often do we mass delete?)
453 if (!empty($this->config
->removeuser
)) {
454 $sql = "SELECT u.id, u.username, u.email
455 FROM {$CFG->prefix}user u
456 LEFT JOIN $temptable e ON u.username = e.username
459 AND e.username IS NULL";
460 $remove_users = get_records_sql($sql);
462 if (!empty($remove_users)) {
463 print "User entries to remove: ". count($remove_users) . "\n";
466 foreach ($remove_users as $user) {
467 if ($this->config
->removeuser
== 2) {
468 //following is copy pasted from admin/user.php
469 //maybe this should moved to function in lib/datalib.php
470 $updateuser = new object();
471 $updateuser->id
= $user->id
;
472 $updateuser->deleted
= 1;
473 $updateuser->username
= addslashes("$user->email.".time()); // Remember it just in case
474 $updateuser->email
= ''; // Clear this field to free it up
475 $updateuser->idnumber
= ''; // Clear this field to free it up
476 $updateuser->timemodified
= time();
477 if (update_record('user', $updateuser)) {
478 delete_records('role_assignments', 'userid', $user->id
); // unassign all roles
479 //copy pasted part ends
480 echo "\t"; print_string('auth_dbdeleteuser', 'auth', array($user->username
, $user->id
)); echo "\n";
482 echo "\t"; print_string('auth_dbdeleteusererror', 'auth', $user->username
); echo "\n";
484 } else if ($this->config
->removeuser
== 1) {
485 $updateuser = new object();
486 $updateuser->id
= $user->id
;
487 $updateuser->auth
= 'nologin';
488 if (update_record('user', $updateuser)) {
489 echo "\t"; print_string('auth_dbsuspenduser', 'auth', array($user->username
, $user->id
)); echo "\n";
491 echo "\t"; print_string('auth_dbsuspendusererror', 'auth', $user->username
); echo "\n";
497 print "No user entries to be removed\n";
499 unset($remove_users); // free mem!
502 /// Revive suspended users
503 if (!empty($this->config
->removeuser
) and $this->config
->removeuser
== 1) {
504 $sql = "SELECT u.id, u.username
505 FROM $temptable e, {$CFG->prefix}user u
506 WHERE e.username=u.username
507 AND u.auth='nologin'";
508 $revive_users = get_records_sql($sql);
510 if (!empty($revive_users)) {
511 print "User entries to be revived: ". count($revive_users) . "\n";
514 foreach ($revive_users as $user) {
515 $updateuser = new object();
516 $updateuser->id
= $user->id
;
517 $updateuser->auth
= 'ldap';
518 if (update_record('user', $updateuser)) {
519 echo "\t"; print_string('auth_dbreviveser', 'auth', array($user->username
, $user->id
)); echo "\n";
521 echo "\t"; print_string('auth_dbreviveusererror', 'auth', $user->username
); echo "\n";
526 print "No user entries to be revived\n";
529 unset($revive_users);
533 /// User Updates - time-consuming (optional)
535 // narrow down what fields we need to update
536 $all_keys = array_keys(get_object_vars($this->config
));
537 $updatekeys = array();
538 foreach ($all_keys as $key) {
539 if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) {
540 // if we have a field to update it from
541 // and it must be updated 'onlogin' we
543 if ( !empty($this->config
->{'field_map_'.$match[1]})
544 and $this->config
->{$match[0]} === 'onlogin') {
545 array_push($updatekeys, $match[1]); // the actual key name
549 // print_r($all_keys); print_r($updatekeys);
550 unset($all_keys); unset($key);
553 print "No updates to be done\n";
555 if ( $do_updates and !empty($updatekeys) ) { // run updates only if relevant
556 $users = get_records_sql("SELECT u.username, u.id
557 FROM {$CFG->prefix}user u
558 WHERE u.deleted=0 AND u.auth='ldap'");
559 if (!empty($users)) {
560 print "User entries to update: ". count($users). "\n";
562 $sitecontext = get_context_instance(CONTEXT_SYSTEM
);
563 if (!empty($this->config
->creators
) and !empty($this->config
->memberattribute
)
564 and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW
)) {
565 $creatorrole = array_shift($roles); // We can only use one, let's use the first one
567 $creatorrole = false;
574 foreach ($users as $user) {
575 echo "\t"; print_string('auth_dbupdatinguser', 'auth', array($user->username
, $user->id
));
576 if (!$this->update_user_record(addslashes($user->username
), $updatekeys)) {
577 echo " - ".get_string('skipped');
582 // update course creators if needed
583 if ($creatorrole !== false) {
584 if ($this->iscreator($user->username
)) {
585 role_assign($creatorrole->id
, $user->id
, 0, $sitecontext->id
, 0, 0, 0, 'ldap');
587 role_unassign($creatorrole->id
, $user->id
, 0, $sitecontext->id
);
591 if ($xcount++
> $maxxcount) {
598 unset($users); // free mem
600 } else { // end do updates
601 print "No updates to be done\n";
605 // find users missing in DB that are in LDAP
606 // note that get_records_sql wants at least 2 fields returned,
607 // and gives me a nifty object I don't want.
608 // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin
609 $sql = "SELECT e.username, e.username
610 FROM $temptable e LEFT JOIN {$CFG->prefix}user u ON e.username = u.username
612 $add_users = get_records_sql($sql); // get rid of the fat
614 if (!empty($add_users)) {
615 print "User entries to add: ". count($add_users). "\n";
617 $sitecontext = get_context_instance(CONTEXT_SYSTEM
);
618 if (!empty($this->config
->creators
) and !empty($this->config
->memberattribute
)
619 and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW
)) {
620 $creatorrole = array_shift($roles); // We can only use one, let's use the first one
622 $creatorrole = false;
626 foreach ($add_users as $user) {
627 $user = $this->get_userinfo_asobj(addslashes($user->username
));
630 $user->modified
= time();
631 $user->confirmed
= 1;
632 $user->auth
= 'ldap';
633 $user->mnethostid
= $CFG->mnet_localhost_id
;
634 if (empty($user->lang
)) {
635 $user->lang
= $CFG->lang
;
638 $user = addslashes_recursive($user);
640 if ($id = insert_record('user',$user)) {
641 echo "\t"; print_string('auth_dbinsertuser', 'auth', array(stripslashes($user->username
), $id)); echo "\n";
642 $userobj = $this->update_user_record($user->username
);
643 if (!empty($this->config
->forcechangepassword
)) {
644 set_user_preference('auth_forcepasswordchange', 1, $userobj->id
);
647 echo "\t"; print_string('auth_dbinsertusererror', 'auth', $user->username
); echo "\n";
650 // add course creators if needed
651 if ($creatorrole !== false and $this->iscreator(stripslashes($user->username
))) {
652 role_assign($creatorrole->id
, $user->id
, 0, $sitecontext->id
, 0, 0, 0, 'ldap');
656 unset($add_users); // free mem
658 print "No users to be added\n";
664 * Update a local user record from an external source.
665 * This is a lighter version of the one in moodlelib -- won't do
666 * expensive ops such as enrolment.
668 * If you don't pass $updatekeys, there is a performance hit and
669 * values removed from LDAP won't be removed from moodle.
671 * @param string $username username (with system magic quotes)
673 function update_user_record($username, $updatekeys = false) {
676 //just in case check text case
677 $username = trim(moodle_strtolower($username));
679 // get the current user record
680 $user = get_record('user', 'username', $username, 'mnethostid', $CFG->mnet_localhost_id
);
681 if (empty($user)) { // trouble
682 error_log("Cannot update non-existent user: ".stripslashes($username));
683 print_error('auth_dbusernotexist','auth',$username);
687 // Protect the userid from being overwritten
690 if ($newinfo = $this->get_userinfo($username)) {
691 $newinfo = truncate_userinfo($newinfo);
693 if (empty($updatekeys)) { // all keys? this does not support removing values
694 $updatekeys = array_keys($newinfo);
697 foreach ($updatekeys as $key) {
698 if (isset($newinfo[$key])) {
699 $value = $newinfo[$key];
704 if (!empty($this->config
->{'field_updatelocal_' . $key})) {
705 if ($user->{$key} != $value) { // only update if it's changed
706 set_field('user', $key, addslashes($value), 'id', $userid);
713 return get_record_select('user', "id = $userid AND deleted = 0");
717 * Bulk insert in SQL's temp table
718 * @param array $users is an array of usernames
720 function ldap_bulk_insert($users, $temptable) {
722 // bulk insert -- superfast with $bulk_insert_records
723 $sql = 'INSERT INTO ' . $temptable . ' (username) VALUES ';
724 // make those values safe
725 $users = addslashes_recursive($users);
726 // join and quote the whole lot
727 $sql = $sql . "('" . implode("'),('", $users) . "')";
728 print "\t+ " . count($users) . " users\n";
729 execute_sql($sql, false);
734 * Activates (enables) user in external db so user can login to external db
736 * @param mixed $username username (with system magic quotes)
737 * @return boolen result
739 function user_activate($username) {
740 $textlib = textlib_get_instance();
741 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config
->ldapencoding
);
743 $ldapconnection = $this->ldap_connect();
745 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
746 switch ($this->config
->user_type
) {
748 $newinfo['loginDisabled']="FALSE";
751 error ('auth: ldap user_activate() does not support selected usertype:"'.$this->config
->user_type
.'" (..yet)');
753 $result = ldap_modify($ldapconnection, $userdn, $newinfo);
754 ldap_close($ldapconnection);
759 * Disables user in external db so user can't login to external db
761 * @param mixed $username username
762 * @return boolean result
764 /* function user_disable($username) {
765 $textlib = textlib_get_instance();
766 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
768 $ldapconnection = $this->ldap_connect();
770 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
771 switch ($this->config->user_type) {
773 $newinfo['loginDisabled']="TRUE";
776 error ('auth: ldap user_disable() does not support selected usertype (..yet)');
778 $result = ldap_modify($ldapconnection, $userdn, $newinfo);
779 ldap_close($ldapconnection);
784 * Returns true if user should be coursecreator.
786 * @param mixed $username username (with system magic quotes)
787 * @return boolean result
789 function iscreator($username = false) {
792 if (empty($this->config
->creators
) or empty($this->config
->memberattribute
)) {
796 if ($username === false) {
797 $username = $USER->username
;
799 $username = stripslashes($username);
802 $textlib = textlib_get_instance();
803 $extusername = $textlib->convert($username, 'utf-8', $this->config
->ldapencoding
);
805 return $this->ldap_isgroupmember($extusername, $this->config
->creators
);
809 * Called when the user record is updated.
810 * Modifies user in external database. It takes olduser (before changes) and newuser (after changes)
811 * conpares information saved modified information to external db.
813 * @param mixed $olduser Userobject before modifications (without system magic quotes)
814 * @param mixed $newuser Userobject new modified userobject (without system magic quotes)
815 * @return boolean result
818 function user_update($olduser, $newuser) {
822 if (isset($olduser->username
) and isset($newuser->username
) and $olduser->username
!= $newuser->username
) {
823 error_log("ERROR:User renaming not allowed in LDAP");
827 if (isset($olduser->auth
) and $olduser->auth
== 'ldap') {
828 return true; // just change auth and skip update
831 $textlib = textlib_get_instance();
832 $extoldusername = $textlib->convert($olduser->username
, 'utf-8', $this->config
->ldapencoding
);
834 $ldapconnection = $this->ldap_connect();
836 $search_attribs = array();
838 $attrmap = $this->ldap_attributes();
839 foreach ($attrmap as $key => $values) {
840 if (!is_array($values)) {
841 $values = array($values);
843 foreach ($values as $value) {
844 if (!in_array($value, $search_attribs)) {
845 array_push($search_attribs, $value);
850 $user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername);
852 $user_info_result = ldap_read($ldapconnection, $user_dn,
853 $this->config
->objectclass
, $search_attribs);
855 if ($user_info_result) {
857 $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
858 if (empty($user_entry)) {
859 return false; // old user not found!
860 } else if (count($user_entry) > 1) {
861 trigger_error("ldap: Strange! More than one user record found in ldap. Only using the first one.");
864 $user_entry = $user_entry[0];
866 //error_log(var_export($user_entry) . 'fpp' );
868 foreach ($attrmap as $key => $ldapkeys) {
869 // only process if the moodle field ($key) has changed and we
870 // are set to update LDAP with it
871 if (isset($olduser->$key) and isset($newuser->$key)
872 and $olduser->$key !== $newuser->$key
873 and !empty($this->config
->{'field_updateremote_'. $key})) {
874 // for ldap values that could be in more than one
875 // ldap key, we will do our best to match
876 // where they came from
879 if (!is_array($ldapkeys)) {
880 $ldapkeys = array($ldapkeys);
882 if (count($ldapkeys) < 2) {
886 $nuvalue = $textlib->convert($newuser->$key, 'utf-8', $this->config
->ldapencoding
);
887 $ouvalue = $textlib->convert($olduser->$key, 'utf-8', $this->config
->ldapencoding
);
889 foreach ($ldapkeys as $ldapkey) {
891 $ldapvalue = $user_entry[$ldapkey][0];
893 // skip update if the values already match
894 if ($nuvalue !== $ldapvalue) {
895 //this might fail due to schema validation
896 if (@ldap_modify
($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
899 error_log('Error updating LDAP record. Error code: '
900 . ldap_errno($ldapconnection) . '; Error string : '
901 . ldap_err2str(ldap_errno($ldapconnection))
902 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
908 // value empty before in Moodle (and LDAP) - use 1st ldap candidate field
910 if ($ouvalue === '') { // value empty before - use 1st ldap candidate
911 //this might fail due to schema validation
912 if (@ldap_modify
($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
916 error_log('Error updating LDAP record. Error code: '
917 . ldap_errno($ldapconnection) . '; Error string : '
918 . ldap_err2str(ldap_errno($ldapconnection))
919 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
924 // we found which ldap key to update!
925 if ($ouvalue !== '' and $ouvalue === $ldapvalue ) {
926 //this might fail due to schema validation
927 if (@ldap_modify
($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
931 error_log('Error updating LDAP record. Error code: '
932 . ldap_errno($ldapconnection) . '; Error string : '
933 . ldap_err2str(ldap_errno($ldapconnection))
934 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
941 if ($ambiguous and !$changed) {
942 error_log("Failed to update LDAP with ambiguous field $key".
943 " old moodle value: '" . $ouvalue .
944 "' new value '" . $nuvalue );
949 error_log("ERROR:No user found in LDAP");
950 @ldap_close
($ldapconnection);
954 @ldap_close
($ldapconnection);
961 * changes userpassword in external db
963 * called when the user password is updated.
964 * changes userpassword in external db
966 * @param object $user User table object (with system magic quotes)
967 * @param string $newpassword Plaintext password (with system magic quotes)
968 * @return boolean result
971 function user_update_password($user, $newpassword) {
972 /// called when the user password is updated -- it assumes it is called by an admin
973 /// or that you've otherwise checked the user's credentials
974 /// IMPORTANT: $newpassword must be cleartext, not crypted/md5'ed
978 $username = $user->username
;
980 $textlib = textlib_get_instance();
981 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config
->ldapencoding
);
982 $extpassword = $textlib->convert(stripslashes($newpassword), 'utf-8', $this->config
->ldapencoding
);
984 $ldapconnection = $this->ldap_connect();
986 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
989 error_log('LDAP Error in user_update_password(). No DN for: ' . stripslashes($user->username
));
993 switch ($this->config
->user_type
) {
996 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
998 error_log('LDAP Error in user_update_password(). Error code: '
999 . ldap_errno($ldapconnection) . '; Error string : '
1000 . ldap_err2str(ldap_errno($ldapconnection)));
1002 //Update password expiration time, grace logins count
1003 $search_attribs = array($this->config
->expireattr
, 'passwordExpirationInterval','loginGraceLimit' );
1004 $sr = ldap_read($ldapconnection, $user_dn, 'objectclass=*', $search_attribs);
1006 $info=$this->ldap_get_entries($ldapconnection, $sr);
1007 $newattrs = array();
1008 if (!empty($info[0][$this->config
->expireattr
][0])) {
1009 //Set expiration time only if passwordExpirationInterval is defined
1010 if (!empty($info[0]['passwordExpirationInterval'][0])) {
1011 $expirationtime = time() +
$info[0]['passwordExpirationInterval'][0];
1012 $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime);
1013 $newattrs['passwordExpirationTime'] = $ldapexpirationtime;
1016 //set gracelogin count
1017 if (!empty($info[0]['loginGraceLimit'][0])) {
1018 $newattrs['loginGraceRemaining']= $info[0]['loginGraceLimit'][0];
1021 //Store attribute changes to ldap
1022 $result = ldap_modify($ldapconnection, $user_dn, $newattrs);
1024 error_log('LDAP Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: '
1025 . ldap_errno($ldapconnection) . '; Error string : '
1026 . ldap_err2str(ldap_errno($ldapconnection)));
1031 error_log('LDAP Error in user_update_password() when reading password expiration time. Error code: '
1032 . ldap_errno($ldapconnection) . '; Error string : '
1033 . ldap_err2str(ldap_errno($ldapconnection)));
1038 $usedconnection = &$ldapconnection;
1039 // send ldap the password in cleartext, it will md5 it itself
1040 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
1042 error_log('LDAP Error in user_update_password(). Error code: '
1043 . ldap_errno($ldapconnection) . '; Error string : '
1044 . ldap_err2str(ldap_errno($ldapconnection)));
1049 @ldap_close
($ldapconnection);
1053 //PRIVATE FUNCTIONS starts
1054 //private functions are named as ldap_*
1057 * returns predefined usertypes
1059 * @return array of predefined usertypes
1061 function ldap_suppported_usertypes() {
1063 $types['edir']='Novell Edirectory';
1064 $types['rfc2307']='posixAccount (rfc2307)';
1065 $types['rfc2307bis']='posixAccount (rfc2307bis)';
1066 $types['samba']='sambaSamAccount (v.3.0.7)';
1067 $types['ad']='MS ActiveDirectory';
1068 $types['default']=get_string('default');
1074 * Initializes needed variables for ldap-module
1076 * Uses names defined in ldap_supported_usertypes.
1077 * $default is first defined as:
1078 * $default['pseudoname'] = array(
1079 * 'typename1' => 'value',
1080 * 'typename2' => 'value'
1084 * @return array of default values
1086 function ldap_getdefaults() {
1087 $default['objectclass'] = array(
1089 'rfc2307' => 'posixAccount',
1090 'rfc2307bis' => 'posixAccount',
1091 'samba' => 'sambaSamAccount',
1095 $default['user_attribute'] = array(
1098 'rfc2307bis' => 'uid',
1103 $default['memberattribute'] = array(
1105 'rfc2307' => 'member',
1106 'rfc2307bis' => 'member',
1107 'samba' => 'member',
1109 'default' => 'member'
1111 $default['memberattribute_isdn'] = array(
1114 'rfc2307bis' => '1',
1115 'samba' => '0', //is this right?
1119 $default['expireattr'] = array (
1120 'edir' => 'passwordExpirationTime',
1121 'rfc2307' => 'shadowExpire',
1122 'rfc2307bis' => 'shadowExpire',
1123 'samba' => '', //No support yet
1124 'ad' => '', //No support yet
1131 * return binaryfields of selected usertype
1136 function ldap_getbinaryfields () {
1137 $binaryfields = array (
1138 'edir' => array('guid'),
1139 'rfc2307' => array(),
1140 'rfc2307bis' => array(),
1143 'default' => array()
1145 if (!empty($this->config
->user_type
)) {
1146 return $binaryfields[$this->config
->user_type
];
1149 return $binaryfields['default'];
1153 function ldap_isbinary ($field) {
1154 if (empty($field)) {
1157 return array_search($field, $this->ldap_getbinaryfields());
1161 * take expirationtime and return it as unixseconds
1163 * takes expriration timestamp as readed from ldap
1164 * returns it as unix seconds
1165 * depends on $this->config->user_type variable
1167 * @param mixed time Time stamp readed from ldap as it is.
1170 function ldap_expirationtime2unix ($time) {
1172 switch ($this->config
->user_type
) {
1174 $yr=substr($time,0,4);
1175 $mo=substr($time,4,2);
1176 $dt=substr($time,6,2);
1177 $hr=substr($time,8,2);
1178 $min=substr($time,10,2);
1179 $sec=substr($time,12,2);
1180 $result = mktime($hr,$min,$sec,$mo,$dt,$yr);
1183 $result = $time * DAYSECS
; //The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date
1186 print_error('auth_ldap_usertypeundefined', 'auth');
1192 * takes unixtime and return it formated for storing in ldap
1194 * @param integer unix time stamp
1196 function ldap_unix2expirationtime($time) {
1198 switch ($this->config
->user_type
) {
1200 $result=date('YmdHis', $time).'Z';
1203 $result = $time ; //Already in correct format
1206 print_error('auth_ldap_usertypeundefined2', 'auth');
1213 * checks if user belong to specific group(s)
1215 * Returns true if user belongs group in grupdns string.
1217 * @param mixed $username username
1218 * @param mixed $groupdns string of group dn separated by ;
1221 function ldap_isgroupmember($extusername='', $groupdns='') {
1222 // Takes username and groupdn(s) , separated by ;
1223 // Returns true if user is member of any given groups
1226 $ldapconnection = $this->ldap_connect();
1228 if (empty($username) or empty($groupdns)) {
1232 if ($this->config
->memberattribute_isdn
) {
1233 $username=$this->ldap_find_userdn($ldapconnection, $username);
1239 $groups = explode(";",$groupdns);
1241 foreach ($groups as $group) {
1242 $group = trim($group);
1243 if (empty($group)) {
1246 //echo "Checking group $group for member $username\n";
1247 $search = @ldap_read
($ldapconnection, $group, '('.$this->config
->memberattribute
.'='.$this->filter_addslashes($username).')', array($this->config
->memberattribute
));
1249 if (!empty($search) and ldap_count_entries($ldapconnection, $search)) {$info = $this->ldap_get_entries($ldapconnection, $search);
1251 if (count($info) > 0 ) {
1252 // user is member of group
1264 * connects to ldap server
1266 * Tries connect to specified ldap servers.
1267 * Returns connection result or error.
1269 * @return connection result
1271 function ldap_connect($binddn='',$bindpwd='') {
1272 //Select bind password, With empty values use
1273 //ldap_bind_* variables or anonymous bind if ldap_bind_* are empty
1274 if ($binddn == '' and $bindpwd == '') {
1275 if (!empty($this->config
->bind_dn
)) {
1276 $binddn = $this->config
->bind_dn
;
1278 if (!empty($this->config
->bind_pw
)) {
1279 $bindpwd = $this->config
->bind_pw
;
1283 $urls = explode(";",$this->config
->host_url
);
1285 foreach ($urls as $server) {
1286 $server = trim($server);
1287 if (empty($server)) {
1291 $connresult = ldap_connect($server);
1292 //ldap_connect returns ALWAYS true
1294 if (!empty($this->config
->version
)) {
1295 ldap_set_option($connresult, LDAP_OPT_PROTOCOL_VERSION
, $this->config
->version
);
1298 if (!empty($binddn)) {
1299 //bind with search-user
1300 //$debuginfo .= 'Using bind user'.$binddn.'and password:'.$bindpwd;
1301 $bindresult=ldap_bind($connresult, $binddn,$bindpwd);
1305 $bindresult=@ldap_bind
($connresult);
1308 if (!empty($this->config
->opt_deref
)) {
1309 ldap_set_option($connresult, LDAP_OPT_DEREF
, $this->config
->opt_deref
);
1316 $debuginfo .= "<br/>Server: '$server' <br/> Connection: '$connresult'<br/> Bind result: '$bindresult'</br>";
1319 //If any of servers are alive we have already returned connection
1320 print_error('auth_ldap_noconnect_all','auth',$this->config
->user_type
);
1325 * retuns dn of username
1327 * Search specified contexts for username and return user dn
1328 * like: cn=username,ou=suborg,o=org
1330 * @param mixed $ldapconnection $ldapconnection result
1331 * @param mixed $username username (external encoding no slashes)
1335 function ldap_find_userdn ($ldapconnection, $extusername) {
1337 //default return value
1338 $ldap_user_dn = FALSE;
1340 //get all contexts and look for first matching user
1341 $ldap_contexts = explode(";",$this->config
->contexts
);
1343 if (!empty($this->config
->create_context
)) {
1344 array_push($ldap_contexts, $this->config
->create_context
);
1347 foreach ($ldap_contexts as $context) {
1349 $context = trim($context);
1350 if (empty($context)) {
1354 if ($this->config
->search_sub
) {
1355 //use ldap_search to find first user from subtree
1356 $ldap_result = ldap_search($ldapconnection, $context, "(".$this->config
->user_attribute
."=".$this->filter_addslashes($extusername).")",array($this->config
->user_attribute
));
1360 //search only in this context
1361 $ldap_result = ldap_list($ldapconnection, $context, "(".$this->config
->user_attribute
."=".$this->filter_addslashes($extusername).")",array($this->config
->user_attribute
));
1364 $entry = ldap_first_entry($ldapconnection,$ldap_result);
1367 $ldap_user_dn = ldap_get_dn($ldapconnection, $entry);
1372 return $ldap_user_dn;
1376 * retuns user attribute mappings between moodle and ldap
1381 function ldap_attributes () {
1382 $fields = array("firstname", "lastname", "email", "phone1", "phone2",
1383 "department", "address", "city", "country", "description",
1384 "idnumber", "lang" );
1385 $moodleattributes = array();
1386 foreach ($fields as $field) {
1387 if (!empty($this->config
->{"field_map_$field"})) {
1388 $moodleattributes[$field] = $this->config
->{"field_map_$field"};
1389 if (preg_match('/,/',$moodleattributes[$field])) {
1390 $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ?
1394 $moodleattributes['username'] = $this->config
->user_attribute
;
1395 return $moodleattributes;
1399 * return all usernames from ldap
1404 function ldap_get_userlist($filter="*") {
1405 /// returns all users from ldap servers
1408 $ldapconnection = $this->ldap_connect();
1411 $filter = "(&(".$this->config
->user_attribute
."=*)(".$this->config
->objectclass
."))";
1414 $contexts = explode(";",$this->config
->contexts
);
1416 if (!empty($this->config
->create_context
)) {
1417 array_push($contexts, $this->config
->create_context
);
1420 foreach ($contexts as $context) {
1422 $context = trim($context);
1423 if (empty($context)) {
1427 if ($this->config
->search_sub
) {
1428 //use ldap_search to find first user from subtree
1429 $ldap_result = ldap_search($ldapconnection, $context,$filter,array($this->config
->user_attribute
));
1432 //search only in this context
1433 $ldap_result = ldap_list($ldapconnection, $context,
1435 array($this->config
->user_attribute
));
1438 $users = $this->ldap_get_entries($ldapconnection, $ldap_result);
1440 //add found users to list
1441 for ($i=0;$i<count($users);$i++
) {
1442 array_push($fresult, ($users[$i][$this->config
->user_attribute
][0]) );
1450 * return entries from ldap
1452 * Returns values like ldap_get_entries but is
1453 * binary compatible and return all attributes as array
1455 * @return array ldap-entries
1458 function ldap_get_entries($conn, $searchresult) {
1459 //Returns values like ldap_get_entries but is
1463 $entry = ldap_first_entry($conn, $searchresult);
1465 $attributes = @ldap_get_attributes
($conn, $entry);
1466 for ($j=0; $j<$attributes['count']; $j++
) {
1467 $values = ldap_get_values_len($conn, $entry,$attributes[$j]);
1468 if (is_array($values)) {
1469 $fresult[$i][$attributes[$j]] = $values;
1472 $fresult[$i][$attributes[$j]] = array($values);
1477 while ($entry = @ldap_next_entry
($conn, $entry));
1483 * Returns true if this authentication plugin is 'internal'.
1487 function is_internal() {
1492 * Returns true if this authentication plugin can change the user's
1497 function can_change_password() {
1498 return !empty($this->config
->stdchangepassword
) or !empty($this->config
->changepasswordurl
);
1502 * Returns the URL for changing the user's pw, or empty if the default can
1505 * @return string url
1507 function change_password_url() {
1508 if (empty($this->config
->stdchangepassword
)) {
1509 return $this->config
->changepasswordurl
;
1516 * Prints a form for configuring this authentication plugin.
1518 * This function is called from admin/auth.php, and outputs a full page with
1519 * a form for configuring this plugin.
1521 * @param array $page An object containing all the data for this page.
1523 function config_form($config, $err, $user_fields) {
1524 include 'config.html';
1528 * Processes and stores configuration data for this authentication plugin.
1530 function process_config($config) {
1531 // set to defaults if undefined
1532 if (!isset($config->host_url
))
1533 { $config->host_url
= ''; }
1534 if (empty($config->ldapencoding
))
1535 { $config->ldapencoding
= 'utf-8'; }
1536 if (!isset($config->contexts
))
1537 { $config->contexts
= ''; }
1538 if (!isset($config->user_type
))
1539 { $config->user_type
= 'default'; }
1540 if (!isset($config->user_attribute
))
1541 { $config->user_attribute
= ''; }
1542 if (!isset($config->search_sub
))
1543 { $config->search_sub
= ''; }
1544 if (!isset($config->opt_deref
))
1545 { $config->opt_deref
= ''; }
1546 if (!isset($config->preventpassindb
))
1547 { $config->preventpassindb
= 0; }
1548 if (!isset($config->bind_dn
))
1549 {$config->bind_dn
= ''; }
1550 if (!isset($config->bind_pw
))
1551 {$config->bind_pw
= ''; }
1552 if (!isset($config->version
))
1553 {$config->version
= '2'; }
1554 if (!isset($config->objectclass
))
1555 {$config->objectclass
= ''; }
1556 if (!isset($config->memberattribute
))
1557 {$config->memberattribute
= ''; }
1558 if (!isset($config->creators
))
1559 {$config->creators
= ''; }
1560 if (!isset($config->create_context
))
1561 {$config->create_context
= ''; }
1562 if (!isset($config->expiration
))
1563 {$config->expiration
= ''; }
1564 if (!isset($config->expiration_warning
))
1565 {$config->expiration_warning
= '10'; }
1566 if (!isset($config->expireattr
))
1567 {$config->expireattr
= ''; }
1568 if (!isset($config->gracelogins
))
1569 {$config->gracelogins
= ''; }
1570 if (!isset($config->graceattr
))
1571 {$config->graceattr
= ''; }
1572 if (!isset($config->auth_user_create
))
1573 {$config->auth_user_create
= ''; }
1574 if (!isset($config->forcechangepassword
))
1575 {$config->forcechangepassword
= 0; }
1576 if (!isset($config->stdchangepassword
))
1577 {$config->stdchangepassword
= 0; }
1578 if (!isset($config->changepasswordurl
))
1579 {$config->changepasswordurl
= ''; }
1580 if (!isset($config->removeuser
))
1581 {$config->removeuser
= 0; }
1584 set_config('host_url', $config->host_url
, 'auth/ldap');
1585 set_config('ldapencoding', $config->ldapencoding
, 'auth/ldap');
1586 set_config('host_url', $config->host_url
, 'auth/ldap');
1587 set_config('contexts', $config->contexts
, 'auth/ldap');
1588 set_config('user_type', $config->user_type
, 'auth/ldap');
1589 set_config('user_attribute', $config->user_attribute
, 'auth/ldap');
1590 set_config('search_sub', $config->search_sub
, 'auth/ldap');
1591 set_config('opt_deref', $config->opt_deref
, 'auth/ldap');
1592 set_config('preventpassindb', $config->preventpassindb
, 'auth/ldap');
1593 set_config('bind_dn', $config->bind_dn
, 'auth/ldap');
1594 set_config('bind_pw', $config->bind_pw
, 'auth/ldap');
1595 set_config('version', $config->version
, 'auth/ldap');
1596 set_config('objectclass', $config->objectclass
, 'auth/ldap');
1597 set_config('memberattribute', $config->memberattribute
, 'auth/ldap');
1598 set_config('creators', $config->creators
, 'auth/ldap');
1599 set_config('create_context', $config->create_context
, 'auth/ldap');
1600 set_config('expiration', $config->expiration
, 'auth/ldap');
1601 set_config('expiration_warning', $config->expiration_warning
, 'auth/ldap');
1602 set_config('expireattr', $config->expireattr
, 'auth/ldap');
1603 set_config('gracelogins', $config->gracelogins
, 'auth/ldap');
1604 set_config('graceattr', $config->graceattr
, 'auth/ldap');
1605 set_config('auth_user_create', $config->auth_user_create
, 'auth/ldap');
1606 set_config('forcechangepassword', $config->forcechangepassword
, 'auth/ldap');
1607 set_config('stdchangepassword', $config->stdchangepassword
, 'auth/ldap');
1608 set_config('changepasswordurl', $config->changepasswordurl
, 'auth/ldap');
1609 set_config('removeuser', $config->removeuser
, 'auth/ldap');
1615 * Quote control characters in texts used in ldap filters - see rfc2254.txt
1619 function filter_addslashes($text) {
1620 $text = str_replace('\\', '\\5c', $text);
1621 $text = str_replace(array('*', '(', ')', "\0"),
1622 array('\\2a', '\\28', '\\29', '\\00'), $text);
1627 * Quote control characters in quoted "texts" used in ldap
1631 function ldap_addslashes($text) {
1632 $text = str_replace('\\', '\\\\', $text);
1633 $text = str_replace(array('"', "\0"),
1634 array('\\"', '\\00'), $text);