MDL-9861 Password expiration value is calculated wrong when ldap_expirationtime2unix...
[moodle.git] / auth / ldap / auth.php
CommitLineData
b9ddb2d5 1<?php
2
3/**
4 * @author Martin Dougiamas
5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
6 * @package moodle multiauth
7 *
8 * Authentication Plugin: LDAP Authentication
9 *
10 * Authentication using LDAP (Lightweight Directory Access Protocol).
11 *
12 * 2006-08-28 File created.
13 */
14
139ebfdb 15if (!defined('MOODLE_INTERNAL')) {
16 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
b9ddb2d5 17}
18
6bc1e5d5 19require_once($CFG->libdir.'/authlib.php');
20
b9ddb2d5 21/**
22 * LDAP authentication plugin.
23 */
6bc1e5d5 24class auth_plugin_ldap extends auth_plugin_base {
b9ddb2d5 25
26 /**
139ebfdb 27 * Constructor with initialisation.
b9ddb2d5 28 */
29 function auth_plugin_ldap() {
6bc1e5d5 30 $this->authtype = 'ldap';
b9ddb2d5 31 $this->config = get_config('auth/ldap');
139ebfdb 32 if (empty($this->config->ldapencoding)) {
33 $this->config->ldapencoding = 'utf-8';
34 }
35 if (empty($this->config->user_type)) {
36 $this->config->user_type = 'default';
37 }
38
39 $default = $this->ldap_getdefaults();
40
41 //use defaults if values not given
42 foreach ($default as $key => $value) {
43 // watch out - 0, false are correct values too
44 if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
45 $this->config->{$key} = $value[$this->config->user_type];
46 }
47 }
48 //hack prefix to objectclass
430759a5 49 if (empty($this->config->objectclass)) { // Can't send empty filter
50 $this->config->objectclass='objectClass=*';
51 } else if (strpos($this->config->objectclass, 'objectClass=') !== 0) {
52 $this->config->objectclass = 'objectClass='.$this->config->objectclass;
139ebfdb 53 }
430759a5 54
b9ddb2d5 55 }
56
57 /**
58 * Returns true if the username and password work and false if they are
59 * wrong or don't exist.
60 *
139ebfdb 61 * @param string $username The username (with system magic quotes)
62 * @param string $password The password (with system magic quotes)
63 *
64 * @return bool Authentication success or failure.
b9ddb2d5 65 */
66 function user_login($username, $password) {
b7b50143 67 if (! function_exists('ldap_bind')) {
43c6650b 68 print_error('auth_ldapnotinstalled','auth');
b7b50143 69 return false;
70 }
b9ddb2d5 71
b9ddb2d5 72 if (!$username or !$password) { // Don't allow blank usernames or passwords
73 return false;
74 }
139ebfdb 75
76 $textlib = textlib_get_instance();
77 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
78 $extpassword = $textlib->convert(stripslashes($password), 'utf-8', $this->config->ldapencoding);
b9ddb2d5 79
80 $ldapconnection = $this->ldap_connect();
81
82 if ($ldapconnection) {
139ebfdb 83 $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
84
b9ddb2d5 85 //if ldap_user_dn is empty, user does not exist
86 if (!$ldap_user_dn) {
87 ldap_close($ldapconnection);
88 return false;
89 }
90
91 // Try to bind with current username and password
139ebfdb 92 $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword);
b9ddb2d5 93 ldap_close($ldapconnection);
94 if ($ldap_login) {
95 return true;
96 }
97 }
98 else {
99 @ldap_close($ldapconnection);
e8b9d76a 100 print_error('auth_ldap_noconnect','auth',$this->config->host_url);
b9ddb2d5 101 }
102 return false;
103 }
104
105 /**
106 * reads userinformation from ldap and return it in array()
107 *
108 * Read user information from external database and returns it as array().
109 * Function should return all information available. If you are saving
110 * this information to moodle user-table you should honor syncronization flags
111 *
139ebfdb 112 * @param string $username username (with system magic quotes)
113 *
114 * @return mixed array with no magic quotes or false on error
b9ddb2d5 115 */
116 function get_userinfo($username) {
139ebfdb 117 $textlib = textlib_get_instance();
118 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
119
b9ddb2d5 120 $ldapconnection = $this->ldap_connect();
b9ddb2d5 121 $attrmap = $this->ldap_attributes();
139ebfdb 122
b9ddb2d5 123 $result = array();
124 $search_attribs = array();
139ebfdb 125
b9ddb2d5 126 foreach ($attrmap as $key=>$values) {
127 if (!is_array($values)) {
128 $values = array($values);
129 }
130 foreach ($values as $value) {
131 if (!in_array($value, $search_attribs)) {
132 array_push($search_attribs, $value);
139ebfdb 133 }
b9ddb2d5 134 }
135 }
136
139ebfdb 137 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
b9ddb2d5 138
139ebfdb 139 if (!$user_info_result = ldap_read($ldapconnection, $user_dn, $this->config->objectclass, $search_attribs)) {
140 return false; // error!
141 }
142 $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
143 if (empty($user_entry)) {
144 return false; // entry not found
145 }
146
147 foreach ($attrmap as $key=>$values) {
148 if (!is_array($values)) {
149 $values = array($values);
150 }
151 $ldapval = NULL;
152 foreach ($values as $value) {
a8d58c58 153 if ($value == 'dn') {
154 $result[$key] = $user_dn;
155 }
139ebfdb 156 if (!array_key_exists($value, $user_entry[0])) {
157 continue; // wrong data mapping!
b9ddb2d5 158 }
139ebfdb 159 if (is_array($user_entry[0][$value])) {
160 $newval = $textlib->convert($user_entry[0][$value][0], $this->config->ldapencoding, 'utf-8');
161 } else {
162 $newval = $textlib->convert($user_entry[0][$value], $this->config->ldapencoding, 'utf-8');
b9ddb2d5 163 }
139ebfdb 164 if (!empty($newval)) { // favour ldap entries that are set
165 $ldapval = $newval;
b9ddb2d5 166 }
167 }
139ebfdb 168 if (!is_null($ldapval)) {
169 $result[$key] = $ldapval;
170 }
b9ddb2d5 171 }
172
173 @ldap_close($ldapconnection);
b9ddb2d5 174 return $result;
175 }
176
177 /**
178 * reads userinformation from ldap and return it in an object
179 *
139ebfdb 180 * @param string $username username (with system magic quotes)
181 * @return mixed object or false on error
b9ddb2d5 182 */
183 function get_userinfo_asobj($username) {
139ebfdb 184 $user_array = $this->get_userinfo($username);
185 if ($user_array == false) {
186 return false; //error or not found
187 }
188 $user_array = truncate_userinfo($user_array);
189 $user = new object();
b9ddb2d5 190 foreach ($user_array as $key=>$value) {
191 $user->{$key} = $value;
192 }
193 return $user;
194 }
195
196 /**
197 * returns all usernames from external database
198 *
199 * get_userlist returns all usernames from external database
200 *
139ebfdb 201 * @return array
b9ddb2d5 202 */
203 function get_userlist() {
b9ddb2d5 204 return $this->ldap_get_userlist("({$this->config->user_attribute}=*)");
205 }
206
207 /**
208 * checks if user exists on external db
139ebfdb 209 *
210 * @param string $username (with system magic quotes)
b9ddb2d5 211 */
212 function user_exists($username) {
139ebfdb 213
214 $textlib = textlib_get_instance();
215 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
216
217 //returns true if given username exist on ldap
218 $users = $this->ldap_get_userlist("({$this->config->user_attribute}=".$this->filter_addslashes($extusername).")");
219 return count($users);
b9ddb2d5 220 }
221
222 /**
139ebfdb 223 * Creates a new user on external database.
b9ddb2d5 224 * By using information in userobject
225 * Use user_exists to prevent dublicate usernames
226 *
139ebfdb 227 * @param mixed $userobject Moodle userobject (with system magic quotes)
228 * @param mixed $plainpass Plaintext password (with system magic quotes)
b9ddb2d5 229 */
230 function user_create($userobject, $plainpass) {
139ebfdb 231 $textlib = textlib_get_instance();
232 $extusername = $textlib->convert(stripslashes($userobject->username), 'utf-8', $this->config->ldapencoding);
233 $extpassword = $textlib->convert(stripslashes($plainpass), 'utf-8', $this->config->ldapencoding);
234
344514fc 235 switch ($this->config->passtype) {
236 case 'md5':
237 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
238 break;
239 case 'sha1':
240 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
241 break;
242 case 'plaintext':
243 default:
244 break; // plaintext
245 }
246
b9ddb2d5 247 $ldapconnection = $this->ldap_connect();
248 $attrmap = $this->ldap_attributes();
139ebfdb 249
b9ddb2d5 250 $newuser = array();
139ebfdb 251
b9ddb2d5 252 foreach ($attrmap as $key => $values) {
253 if (!is_array($values)) {
254 $values = array($values);
255 }
256 foreach ($values as $value) {
257 if (!empty($userobject->$key) ) {
139ebfdb 258 $newuser[$value] = $textlib->convert(stripslashes($userobject->$key), 'utf-8', $this->config->ldapencoding);
b9ddb2d5 259 }
260 }
261 }
139ebfdb 262
b9ddb2d5 263 //Following sets all mandatory and other forced attribute values
264 //User should be creted as login disabled untill email confirmation is processed
139ebfdb 265 //Feel free to add your user type and send patches to paca@sci.fi to add them
b9ddb2d5 266 //Moodle distribution
267
268 switch ($this->config->user_type) {
269 case 'edir':
139ebfdb 270 $newuser['objectClass'] = array("inetOrgPerson","organizationalPerson","person","top");
271 $newuser['uniqueId'] = $extusername;
272 $newuser['logindisabled'] = "TRUE";
273 $newuser['userpassword'] = $extpassword;
b9ddb2d5 274 break;
275 default:
e8b9d76a 276 print_error('auth_ldap_unsupportedusertype','auth',$this->config->user_type);
b9ddb2d5 277 }
139ebfdb 278 $uadd = $this->ldap_add($ldapconnection, $this->config->user_attribute.'="'.$this->ldap_addslashes($userobject->username).','.$this->config->create_context.'"', $newuser);
b9ddb2d5 279 ldap_close($ldapconnection);
280 return $uadd;
b9ddb2d5 281
b9ddb2d5 282 }
283
284 /**
285 * return number of days to user password expires
286 *
287 * If userpassword does not expire it should return 0. If password is already expired
288 * it should return negative value.
289 *
6bc1e5d5 290 * @param mixed $username username (with system magic quotes)
b9ddb2d5 291 * @return integer
292 */
293 function password_expire($username) {
2cef74f9 294 $result = 0;
139ebfdb 295
296 $textlib = textlib_get_instance();
297 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
298
b9ddb2d5 299 $ldapconnection = $this->ldap_connect();
139ebfdb 300 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
b9ddb2d5 301 $search_attribs = array($this->config->expireattr);
302 $sr = ldap_read($ldapconnection, $user_dn, 'objectclass=*', $search_attribs);
303 if ($sr) {
139ebfdb 304 $info = $this->ldap_get_entries($ldapconnection, $sr);
2cef74f9 305 if (!empty ($info) and !empty($info[0][$this->config->expireattr][0])) {
306 $expiretime = $this->ldap_expirationtime2unix($info[0][$this->config->expireattr][0], $ldapconnection, $user_dn);
307 if ($expiretime != 0) {
308 $now = time();
309 if ($expiretime > $now) {
310 $result = ceil(($expiretime - $now) / DAYSECS);
311 }
312 else {
313 $result = floor(($expiretime - $now) / DAYSECS);
314 }
139ebfdb 315 }
b9ddb2d5 316 }
139ebfdb 317 } else {
b9ddb2d5 318 error_log("ldap: password_expire did't find expiration time.");
319 }
320
321 //error_log("ldap: password_expire user $user_dn expires in $result days!");
322 return $result;
323 }
324
325 /**
326 * syncronizes user fron external db to moodle user table
327 *
139ebfdb 328 * Sync is now using username attribute.
329 *
330 * Syncing users removes or suspends users that dont exists anymore in external db.
331 * Creates new users and updates coursecreator status of users.
332 *
333 * @param int $bulk_insert_records will insert $bulkinsert_records per insert statement
334 * valid only with $unsafe. increase to a couple thousand for
335 * blinding fast inserts -- but test it: you may hit mysqld's
336 * max_allowed_packet limit.
337 * @param bool $do_updates will do pull in data updates from ldap if relevant
b9ddb2d5 338 */
139ebfdb 339 function sync_users ($bulk_insert_records = 1000, $do_updates = true) {
b9ddb2d5 340
fa96bfaa 341 global $CFG;
342
139ebfdb 343 $textlib = textlib_get_instance();
344
fa96bfaa 345 $droptablesql = array(); /// sql commands to drop the table (because session scope could be a problem for
346 /// some persistent drivers like ODBTP (mssql) or if this function is invoked
347 /// from within a PHP application using persistent connections
b9ddb2d5 348
139ebfdb 349 // configure a temp table
350 print "Configuring temp table\n";
fa96bfaa 351 switch (strtolower($CFG->dbfamily)) {
352 case 'mysql':
353 $temptable = $CFG->prefix . 'extuser';
354 $droptablesql[] = 'DROP TEMPORARY TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
355 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
356 echo "Creating temp table $temptable\n";
139ebfdb 357 execute_sql('CREATE TEMPORARY TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username)) TYPE=MyISAM', false);
fa96bfaa 358 break;
359 case 'postgres':
360 $temptable = $CFG->prefix . 'extuser';
361 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
362 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
363 echo "Creating temp table $temptable\n";
364 $bulk_insert_records = 1; // no support for multiple sets of values
139ebfdb 365 execute_sql('CREATE TEMPORARY TABLE '. $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))', false);
fa96bfaa 366 break;
367 case 'mssql':
368 $temptable = '#'.$CFG->prefix . 'extuser'; /// MSSQL temp tables begin with #
369 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
370 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
371 echo "Creating temp table $temptable\n";
372 $bulk_insert_records = 1; // no support for multiple sets of values
139ebfdb 373 execute_sql('CREATE TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))', false);
fa96bfaa 374 break;
375 case 'oracle':
376 $temptable = $CFG->prefix . 'extuser';
377 $droptablesql[] = 'TRUNCATE TABLE ' . $temptable; // oracle requires truncate before being able to drop a temp table
378 $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
379 execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
380 echo "Creating temp table $temptable\n";
381 $bulk_insert_records = 1; // no support for multiple sets of values
139ebfdb 382 execute_sql('CREATE GLOBAL TEMPORARY TABLE '.$temptable.' (username VARCHAR(64), PRIMARY KEY (username)) ON COMMIT PRESERVE ROWS', false);
fa96bfaa 383 break;
b9ddb2d5 384 }
385
139ebfdb 386 print "Connecting to ldap...\n";
b9ddb2d5 387 $ldapconnection = $this->ldap_connect();
388
389 if (!$ldapconnection) {
390 @ldap_close($ldapconnection);
139ebfdb 391 print get_string('auth_ldap_noconnect','auth',$this->config->host_url);
392 exit;
b9ddb2d5 393 }
394
395 ////
396 //// get user's list from ldap to sql in a scalable fashion
397 ////
398 // prepare some data we'll need
b9ddb2d5 399 $filter = "(&(".$this->config->user_attribute."=*)(".$this->config->objectclass."))";
400
401 $contexts = explode(";",$this->config->contexts);
139ebfdb 402
b9ddb2d5 403 if (!empty($this->config->create_context)) {
404 array_push($contexts, $this->config->create_context);
405 }
406
407 $fresult = array();
b9ddb2d5 408 foreach ($contexts as $context) {
409 $context = trim($context);
410 if (empty($context)) {
411 continue;
412 }
413 begin_sql();
414 if ($this->config->search_sub) {
415 //use ldap_search to find first user from subtree
416 $ldap_result = ldap_search($ldapconnection, $context,
417 $filter,
418 array($this->config->user_attribute));
139ebfdb 419 } else {
b9ddb2d5 420 //search only in this context
421 $ldap_result = ldap_list($ldapconnection, $context,
422 $filter,
423 array($this->config->user_attribute));
424 }
425
426 if ($entry = ldap_first_entry($ldapconnection, $ldap_result)) {
427 do {
139ebfdb 428 $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute);
429 $value = $textlib->convert($value[0], $this->config->ldapencoding, 'utf-8');
b9ddb2d5 430 array_push($fresult, $value);
431 if (count($fresult) >= $bulk_insert_records) {
fa96bfaa 432 $this->ldap_bulk_insert($fresult, $temptable);
139ebfdb 433 $fresult = array();
434 }
435 } while ($entry = ldap_next_entry($ldapconnection, $entry));
b9ddb2d5 436 }
139ebfdb 437 unset($ldap_result); // free mem
b9ddb2d5 438
439 // insert any remaining users and release mem
440 if (count($fresult)) {
fa96bfaa 441 $this->ldap_bulk_insert($fresult, $temptable);
139ebfdb 442 $fresult = array();
b9ddb2d5 443 }
444 commit_sql();
445 }
b9ddb2d5 446
447 /// preserve our user database
448 /// if the temp table is empty, it probably means that something went wrong, exit
449 /// so as to avoid mass deletion of users; which is hard to undo
139ebfdb 450 $count = get_record_sql('SELECT COUNT(username) AS count, 1 FROM ' . $temptable);
b9ddb2d5 451 $count = $count->{'count'};
452 if ($count < 1) {
453 print "Did not get any users from LDAP -- error? -- exiting\n";
454 exit;
fa96bfaa 455 } else {
139ebfdb 456 print "Got $count records from LDAP\n\n";
b9ddb2d5 457 }
458
b9ddb2d5 459
139ebfdb 460/// User removal
461 // find users in DB that aren't in ldap -- to be removed!
462 // this is still not as scalable (but how often do we mass delete?)
463 if (!empty($this->config->removeuser)) {
464 $sql = "SELECT u.id, u.username, u.email
465 FROM {$CFG->prefix}user u
466 LEFT JOIN $temptable e ON u.username = e.username
467 WHERE u.auth='ldap'
468 AND u.deleted=0
469 AND e.username IS NULL";
470 $remove_users = get_records_sql($sql);
471
472 if (!empty($remove_users)) {
473 print "User entries to remove: ". count($remove_users) . "\n";
474
475 begin_sql();
476 foreach ($remove_users as $user) {
477 if ($this->config->removeuser == 2) {
478 //following is copy pasted from admin/user.php
479 //maybe this should moved to function in lib/datalib.php
480 $updateuser = new object();
481 $updateuser->id = $user->id;
482 $updateuser->deleted = 1;
483 $updateuser->username = addslashes("$user->email.".time()); // Remember it just in case
484 $updateuser->email = ''; // Clear this field to free it up
485 $updateuser->idnumber = ''; // Clear this field to free it up
486 $updateuser->timemodified = time();
487 if (update_record('user', $updateuser)) {
488 delete_records('role_assignments', 'userid', $user->id); // unassign all roles
489 //copy pasted part ends
490 echo "\t"; print_string('auth_dbdeleteuser', 'auth', array($user->username, $user->id)); echo "\n";
491 } else {
492 echo "\t"; print_string('auth_dbdeleteusererror', 'auth', $user->username); echo "\n";
493 }
494 } else if ($this->config->removeuser == 1) {
495 $updateuser = new object();
496 $updateuser->id = $user->id;
497 $updateuser->auth = 'nologin';
498 if (update_record('user', $updateuser)) {
499 echo "\t"; print_string('auth_dbsuspenduser', 'auth', array($user->username, $user->id)); echo "\n";
500 } else {
501 echo "\t"; print_string('auth_dbsuspendusererror', 'auth', $user->username); echo "\n";
502 }
503 }
b9ddb2d5 504 }
139ebfdb 505 commit_sql();
506 } else {
507 print "No user entries to be removed\n";
508 }
509 unset($remove_users); // free mem!
510 }
511
512/// Revive suspended users
513 if (!empty($this->config->removeuser) and $this->config->removeuser == 1) {
514 $sql = "SELECT u.id, u.username
515 FROM $temptable e, {$CFG->prefix}user u
516 WHERE e.username=u.username
517 AND u.auth='nologin'";
518 $revive_users = get_records_sql($sql);
519
520 if (!empty($revive_users)) {
521 print "User entries to be revived: ". count($revive_users) . "\n";
522
523 begin_sql();
524 foreach ($revive_users as $user) {
525 $updateuser = new object();
526 $updateuser->id = $user->id;
527 $updateuser->auth = 'ldap';
528 if (update_record('user', $updateuser)) {
529 echo "\t"; print_string('auth_dbreviveser', 'auth', array($user->username, $user->id)); echo "\n";
530 } else {
531 echo "\t"; print_string('auth_dbreviveusererror', 'auth', $user->username); echo "\n";
532 }
b9ddb2d5 533 }
139ebfdb 534 commit_sql();
535 } else {
536 print "No user entries to be revived\n";
537 }
538
539 unset($revive_users);
fa96bfaa 540 }
b9ddb2d5 541
139ebfdb 542
543/// User Updates - time-consuming (optional)
b9ddb2d5 544 if ($do_updates) {
545 // narrow down what fields we need to update
546 $all_keys = array_keys(get_object_vars($this->config));
547 $updatekeys = array();
548 foreach ($all_keys as $key) {
549 if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) {
550 // if we have a field to update it from
139ebfdb 551 // and it must be updated 'onlogin' we
b9ddb2d5 552 // update it on cron
553 if ( !empty($this->config->{'field_map_'.$match[1]})
139ebfdb 554 and $this->config->{$match[0]} === 'onlogin') {
b9ddb2d5 555 array_push($updatekeys, $match[1]); // the actual key name
556 }
557 }
558 }
559 // print_r($all_keys); print_r($updatekeys);
560 unset($all_keys); unset($key);
139ebfdb 561
fa96bfaa 562 } else {
563 print "No updates to be done\n";
b9ddb2d5 564 }
139ebfdb 565 if ( $do_updates and !empty($updatekeys) ) { // run updates only if relevant
566 $users = get_records_sql("SELECT u.username, u.id
567 FROM {$CFG->prefix}user u
568 WHERE u.deleted=0 AND u.auth='ldap'");
b9ddb2d5 569 if (!empty($users)) {
570 print "User entries to update: ". count($users). "\n";
139ebfdb 571
b9ddb2d5 572 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
139ebfdb 573 if (!empty($this->config->creators) and !empty($this->config->memberattribute)
574 and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
575 $creatorrole = array_shift($roles); // We can only use one, let's use the first one
576 } else {
577 $creatorrole = false;
578 }
b9ddb2d5 579
139ebfdb 580 begin_sql();
581 $xcount = 0;
582 $maxxcount = 100;
b9ddb2d5 583
139ebfdb 584 foreach ($users as $user) {
585 echo "\t"; print_string('auth_dbupdatinguser', 'auth', array($user->username, $user->id));
586 if (!$this->update_user_record(addslashes($user->username), $updatekeys)) {
587 echo " - ".get_string('skipped');
588 }
589 echo "\n";
590 $xcount++;
591
592 // update course creators if needed
593 if ($creatorrole !== false) {
594 if ($this->iscreator($user->username)) {
595 role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
596 } else {
6bc1e5d5 597 role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'ldap');
b9ddb2d5 598 }
139ebfdb 599 }
600
601 if ($xcount++ > $maxxcount) {
602 commit_sql();
603 begin_sql();
604 $xcount = 0;
605 }
b9ddb2d5 606 }
139ebfdb 607 commit_sql();
608 unset($users); // free mem
b9ddb2d5 609 }
fa96bfaa 610 } else { // end do updates
611 print "No updates to be done\n";
612 }
139ebfdb 613
614/// User Additions
b9ddb2d5 615 // find users missing in DB that are in LDAP
616 // note that get_records_sql wants at least 2 fields returned,
617 // and gives me a nifty object I don't want.
139ebfdb 618 // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin
619 $sql = "SELECT e.username, e.username
620 FROM $temptable e LEFT JOIN {$CFG->prefix}user u ON e.username = u.username
621 WHERE u.id IS NULL";
622 $add_users = get_records_sql($sql); // get rid of the fat
623
b9ddb2d5 624 if (!empty($add_users)) {
625 print "User entries to add: ". count($add_users). "\n";
626
139ebfdb 627 $sitecontext = get_context_instance(CONTEXT_SYSTEM);
628 if (!empty($this->config->creators) and !empty($this->config->memberattribute)
629 and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
b9ddb2d5 630 $creatorrole = array_shift($roles); // We can only use one, let's use the first one
139ebfdb 631 } else {
632 $creatorrole = false;
b9ddb2d5 633 }
634
635 begin_sql();
636 foreach ($add_users as $user) {
139ebfdb 637 $user = $this->get_userinfo_asobj(addslashes($user->username));
638
b9ddb2d5 639 // prep a few params
b7b50143 640 $user->modified = time();
641 $user->confirmed = 1;
139ebfdb 642 $user->auth = 'ldap';
b7b50143 643 $user->mnethostid = $CFG->mnet_localhost_id;
139ebfdb 644 if (empty($user->lang)) {
645 $user->lang = $CFG->lang;
b9ddb2d5 646 }
139ebfdb 647
648 $user = addslashes_recursive($user);
649
650 if ($id = insert_record('user',$user)) {
651 echo "\t"; print_string('auth_dbinsertuser', 'auth', array(stripslashes($user->username), $id)); echo "\n";
652 $userobj = $this->update_user_record($user->username);
653 if (!empty($this->config->forcechangepassword)) {
654 set_user_preference('auth_forcepasswordchange', 1, $userobj->id);
b9ddb2d5 655 }
139ebfdb 656 } else {
657 echo "\t"; print_string('auth_dbinsertusererror', 'auth', $user->username); echo "\n";
658 }
659
660 // add course creators if needed
661 if ($creatorrole !== false and $this->iscreator(stripslashes($user->username))) {
662 role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
b9ddb2d5 663 }
664 }
665 commit_sql();
666 unset($add_users); // free mem
fa96bfaa 667 } else {
668 print "No users to be added\n";
b9ddb2d5 669 }
670 return true;
671 }
672
139ebfdb 673 /**
674 * Update a local user record from an external source.
675 * This is a lighter version of the one in moodlelib -- won't do
b9ddb2d5 676 * expensive ops such as enrolment.
677 *
139ebfdb 678 * If you don't pass $updatekeys, there is a performance hit and
679 * values removed from LDAP won't be removed from moodle.
680 *
681 * @param string $username username (with system magic quotes)
b9ddb2d5 682 */
683 function update_user_record($username, $updatekeys = false) {
b9ddb2d5 684 global $CFG;
685
686 //just in case check text case
687 $username = trim(moodle_strtolower($username));
139ebfdb 688
b9ddb2d5 689 // get the current user record
b7b50143 690 $user = get_record('user', 'username', $username, 'mnethostid', $CFG->mnet_localhost_id);
b9ddb2d5 691 if (empty($user)) { // trouble
139ebfdb 692 error_log("Cannot update non-existent user: ".stripslashes($username));
693 print_error('auth_dbusernotexist','auth',$username);
b9ddb2d5 694 die;
695 }
696
b7b50143 697 // Protect the userid from being overwritten
698 $userid = $user->id;
699
139ebfdb 700 if ($newinfo = $this->get_userinfo($username)) {
701 $newinfo = truncate_userinfo($newinfo);
702
703 if (empty($updatekeys)) { // all keys? this does not support removing values
704 $updatekeys = array_keys($newinfo);
705 }
706
707 foreach ($updatekeys as $key) {
708 if (isset($newinfo[$key])) {
709 $value = $newinfo[$key];
710 } else {
711 $value = '';
b9ddb2d5 712 }
139ebfdb 713
714 if (!empty($this->config->{'field_updatelocal_' . $key})) {
715 if ($user->{$key} != $value) { // only update if it's changed
716 set_field('user', $key, addslashes($value), 'id', $userid);
b9ddb2d5 717 }
718 }
719 }
139ebfdb 720 } else {
721 return false;
b9ddb2d5 722 }
139ebfdb 723 return get_record_select('user', "id = $userid AND deleted = 0");
b9ddb2d5 724 }
725
139ebfdb 726 /**
727 * Bulk insert in SQL's temp table
728 * @param array $users is an array of usernames
729 */
fa96bfaa 730 function ldap_bulk_insert($users, $temptable) {
731
b9ddb2d5 732 // bulk insert -- superfast with $bulk_insert_records
139ebfdb 733 $sql = 'INSERT INTO ' . $temptable . ' (username) VALUES ';
b9ddb2d5 734 // make those values safe
139ebfdb 735 $users = addslashes_recursive($users);
b9ddb2d5 736 // join and quote the whole lot
139ebfdb 737 $sql = $sql . "('" . implode("'),('", $users) . "')";
738 print "\t+ " . count($users) . " users\n";
739 execute_sql($sql, false);
b9ddb2d5 740 }
741
742
139ebfdb 743 /**
b9ddb2d5 744 * Activates (enables) user in external db so user can login to external db
745 *
139ebfdb 746 * @param mixed $username username (with system magic quotes)
b9ddb2d5 747 * @return boolen result
748 */
749 function user_activate($username) {
139ebfdb 750 $textlib = textlib_get_instance();
751 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
752
b9ddb2d5 753 $ldapconnection = $this->ldap_connect();
754
139ebfdb 755 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
b9ddb2d5 756 switch ($this->config->user_type) {
757 case 'edir':
758 $newinfo['loginDisabled']="FALSE";
759 break;
760 default:
139ebfdb 761 error ('auth: ldap user_activate() does not support selected usertype:"'.$this->config->user_type.'" (..yet)');
762 }
b9ddb2d5 763 $result = ldap_modify($ldapconnection, $userdn, $newinfo);
764 ldap_close($ldapconnection);
765 return $result;
766 }
767
139ebfdb 768 /**
b9ddb2d5 769 * Disables user in external db so user can't login to external db
770 *
771 * @param mixed $username username
772 * @return boolean result
773 */
139ebfdb 774/* function user_disable($username) {
775 $textlib = textlib_get_instance();
776 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
b9ddb2d5 777
778 $ldapconnection = $this->ldap_connect();
779
139ebfdb 780 $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
b9ddb2d5 781 switch ($this->config->user_type) {
782 case 'edir':
783 $newinfo['loginDisabled']="TRUE";
784 break;
785 default:
139ebfdb 786 error ('auth: ldap user_disable() does not support selected usertype (..yet)');
787 }
b9ddb2d5 788 $result = ldap_modify($ldapconnection, $userdn, $newinfo);
789 ldap_close($ldapconnection);
790 return $result;
139ebfdb 791 }*/
b9ddb2d5 792
139ebfdb 793 /**
b9ddb2d5 794 * Returns true if user should be coursecreator.
795 *
6bc1e5d5 796 * @param mixed $username username (without system magic quotes)
b9ddb2d5 797 * @return boolean result
798 */
6bc1e5d5 799 function iscreator($username) {
139ebfdb 800 if (empty($this->config->creators) or empty($this->config->memberattribute)) {
6bc1e5d5 801 return null;
b9ddb2d5 802 }
139ebfdb 803
804 $textlib = textlib_get_instance();
805 $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
806
6bc1e5d5 807 return (boolean)$this->ldap_isgroupmember($extusername, $this->config->creators);
b9ddb2d5 808 }
809
139ebfdb 810 /**
b9ddb2d5 811 * Called when the user record is updated.
139ebfdb 812 * Modifies user in external database. It takes olduser (before changes) and newuser (after changes)
b9ddb2d5 813 * conpares information saved modified information to external db.
814 *
139ebfdb 815 * @param mixed $olduser Userobject before modifications (without system magic quotes)
816 * @param mixed $newuser Userobject new modified userobject (without system magic quotes)
b9ddb2d5 817 * @return boolean result
818 *
819 */
820 function user_update($olduser, $newuser) {
821
139ebfdb 822 global $USER;
823
824 if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) {
825 error_log("ERROR:User renaming not allowed in LDAP");
826 return false;
827 }
828
6bc1e5d5 829 if (isset($olduser->auth) and $olduser->auth != 'ldap') {
139ebfdb 830 return true; // just change auth and skip update
831 }
832
833 $textlib = textlib_get_instance();
834 $extoldusername = $textlib->convert($olduser->username, 'utf-8', $this->config->ldapencoding);
b9ddb2d5 835
836 $ldapconnection = $this->ldap_connect();
139ebfdb 837
b9ddb2d5 838 $search_attribs = array();
839
139ebfdb 840 $attrmap = $this->ldap_attributes();
b9ddb2d5 841 foreach ($attrmap as $key => $values) {
842 if (!is_array($values)) {
843 $values = array($values);
844 }
845 foreach ($values as $value) {
846 if (!in_array($value, $search_attribs)) {
847 array_push($search_attribs, $value);
848 }
139ebfdb 849 }
b9ddb2d5 850 }
851
139ebfdb 852 $user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername);
b9ddb2d5 853
854 $user_info_result = ldap_read($ldapconnection, $user_dn,
855 $this->config->objectclass, $search_attribs);
856
857 if ($user_info_result) {
858
859 $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
139ebfdb 860 if (empty($user_entry)) {
861 return false; // old user not found!
862 } else if (count($user_entry) > 1) {
b9ddb2d5 863 trigger_error("ldap: Strange! More than one user record found in ldap. Only using the first one.");
139ebfdb 864 return false;
b9ddb2d5 865 }
866 $user_entry = $user_entry[0];
867
868 //error_log(var_export($user_entry) . 'fpp' );
b9ddb2d5 869
139ebfdb 870 foreach ($attrmap as $key => $ldapkeys) {
b9ddb2d5 871 // only process if the moodle field ($key) has changed and we
872 // are set to update LDAP with it
139ebfdb 873 if (isset($olduser->$key) and isset($newuser->$key)
874 and $olduser->$key !== $newuser->$key
875 and !empty($this->config->{'field_updateremote_'. $key})) {
876 // for ldap values that could be in more than one
877 // ldap key, we will do our best to match
b9ddb2d5 878 // where they came from
879 $ambiguous = true;
880 $changed = false;
881 if (!is_array($ldapkeys)) {
882 $ldapkeys = array($ldapkeys);
883 }
884 if (count($ldapkeys) < 2) {
885 $ambiguous = false;
886 }
139ebfdb 887
888 $nuvalue = $textlib->convert($newuser->$key, 'utf-8', $this->config->ldapencoding);
889 $ouvalue = $textlib->convert($olduser->$key, 'utf-8', $this->config->ldapencoding);
890
b9ddb2d5 891 foreach ($ldapkeys as $ldapkey) {
139ebfdb 892 $ldapkey = $ldapkey;
b9ddb2d5 893 $ldapvalue = $user_entry[$ldapkey][0];
894 if (!$ambiguous) {
895 // skip update if the values already match
139ebfdb 896 if ($nuvalue !== $ldapvalue) {
897 //this might fail due to schema validation
898 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
899 continue;
900 } else {
901 error_log('Error updating LDAP record. Error code: '
902 . ldap_errno($ldapconnection) . '; Error string : '
903 . ldap_err2str(ldap_errno($ldapconnection))
904 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
905 continue;
906 }
b9ddb2d5 907 }
139ebfdb 908 } else {
b9ddb2d5 909 // ambiguous
910 // value empty before in Moodle (and LDAP) - use 1st ldap candidate field
911 // no need to guess
139ebfdb 912 if ($ouvalue === '') { // value empty before - use 1st ldap candidate
913 //this might fail due to schema validation
914 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
b9ddb2d5 915 $changed = true;
139ebfdb 916 continue;
917 } else {
918 error_log('Error updating LDAP record. Error code: '
919 . ldap_errno($ldapconnection) . '; Error string : '
920 . ldap_err2str(ldap_errno($ldapconnection))
921 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
922 continue;
b9ddb2d5 923 }
924 }
925
139ebfdb 926 // we found which ldap key to update!
927 if ($ouvalue !== '' and $ouvalue === $ldapvalue ) {
928 //this might fail due to schema validation
929 if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
b9ddb2d5 930 $changed = true;
139ebfdb 931 continue;
932 } else {
933 error_log('Error updating LDAP record. Error code: '
b9ddb2d5 934 . ldap_errno($ldapconnection) . '; Error string : '
139ebfdb 935 . ldap_err2str(ldap_errno($ldapconnection))
936 . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
937 continue;
b9ddb2d5 938 }
939 }
940 }
941 }
139ebfdb 942
b9ddb2d5 943 if ($ambiguous and !$changed) {
139ebfdb 944 error_log("Failed to update LDAP with ambiguous field $key".
945 " old moodle value: '" . $ouvalue .
946 "' new value '" . $nuvalue );
b9ddb2d5 947 }
948 }
949 }
139ebfdb 950 } else {
b9ddb2d5 951 error_log("ERROR:No user found in LDAP");
952 @ldap_close($ldapconnection);
953 return false;
954 }
955
956 @ldap_close($ldapconnection);
139ebfdb 957
b9ddb2d5 958 return true;
959
960 }
961
fb5c7739 962 /**
b9ddb2d5 963 * changes userpassword in external db
964 *
965 * called when the user password is updated.
966 * changes userpassword in external db
967 *
139ebfdb 968 * @param object $user User table object (with system magic quotes)
969 * @param string $newpassword Plaintext password (with system magic quotes)
b9ddb2d5 970 * @return boolean result
971 *
972 */
b9ddb2d5 973 function user_update_password($user, $newpassword) {
974 /// called when the user password is updated -- it assumes it is called by an admin
975 /// or that you've otherwise checked the user's credentials
976 /// IMPORTANT: $newpassword must be cleartext, not crypted/md5'ed
977
139ebfdb 978 global $USER;
b9ddb2d5 979 $result = false;
980 $username = $user->username;
139ebfdb 981
982 $textlib = textlib_get_instance();
983 $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
984 $extpassword = $textlib->convert(stripslashes($newpassword), 'utf-8', $this->config->ldapencoding);
985
344514fc 986 switch ($this->config->passtype) {
987 case 'md5':
988 $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
989 break;
990 case 'sha1':
991 $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
992 break;
993 case 'plaintext':
994 default:
995 break; // plaintext
996 }
997
b9ddb2d5 998 $ldapconnection = $this->ldap_connect();
999
139ebfdb 1000 $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
1001
b9ddb2d5 1002 if (!$user_dn) {
139ebfdb 1003 error_log('LDAP Error in user_update_password(). No DN for: ' . stripslashes($user->username));
b9ddb2d5 1004 return false;
1005 }
1006
1007 switch ($this->config->user_type) {
1008 case 'edir':
1009 //Change password
139ebfdb 1010 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
b9ddb2d5 1011 if (!$result) {
1012 error_log('LDAP Error in user_update_password(). Error code: '
1013 . ldap_errno($ldapconnection) . '; Error string : '
1014 . ldap_err2str(ldap_errno($ldapconnection)));
1015 }
1016 //Update password expiration time, grace logins count
1017 $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval','loginGraceLimit' );
1018 $sr = ldap_read($ldapconnection, $user_dn, 'objectclass=*', $search_attribs);
1019 if ($sr) {
1020 $info=$this->ldap_get_entries($ldapconnection, $sr);
1021 $newattrs = array();
1022 if (!empty($info[0][$this->config->expireattr][0])) {
1023 //Set expiration time only if passwordExpirationInterval is defined
1024 if (!empty($info[0]['passwordExpirationInterval'][0])) {
139ebfdb 1025 $expirationtime = time() + $info[0]['passwordExpirationInterval'][0];
b9ddb2d5 1026 $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime);
1027 $newattrs['passwordExpirationTime'] = $ldapexpirationtime;
139ebfdb 1028 }
b9ddb2d5 1029
1030 //set gracelogin count
1031 if (!empty($info[0]['loginGraceLimit'][0])) {
139ebfdb 1032 $newattrs['loginGraceRemaining']= $info[0]['loginGraceLimit'][0];
b9ddb2d5 1033 }
139ebfdb 1034
b9ddb2d5 1035 //Store attribute changes to ldap
1036 $result = ldap_modify($ldapconnection, $user_dn, $newattrs);
1037 if (!$result) {
1038 error_log('LDAP Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: '
1039 . ldap_errno($ldapconnection) . '; Error string : '
1040 . ldap_err2str(ldap_errno($ldapconnection)));
1041 }
1042 }
1043 }
1044 else {
1045 error_log('LDAP Error in user_update_password() when reading password expiration time. Error code: '
1046 . ldap_errno($ldapconnection) . '; Error string : '
1047 . ldap_err2str(ldap_errno($ldapconnection)));
d0e84e1b 1048 }
1049 break;
1050
1051 case 'ad':
1052 // Passwords in Active Directory must be encoded as Unicode
1053 // strings (UCS-2 Little Endian format) and surrounded with
1054 // double quotes. See http://support.microsoft.com/?kbid=269190
1055 if (!function_exists('mb_convert_encoding')) {
1056 error_log ('You need the mbstring extension to change passwords in Active Directory');
1057 return false;
1058 }
1059 $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding);
1060 $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword));
1061 if (!$result) {
1062 error_log('LDAP Error in user_update_password(). Error code: '
1063 . ldap_errno($ldapconnection) . '; Error string : '
1064 . ldap_err2str(ldap_errno($ldapconnection)));
139ebfdb 1065 }
b9ddb2d5 1066 break;
139ebfdb 1067
b9ddb2d5 1068 default:
1069 $usedconnection = &$ldapconnection;
1070 // send ldap the password in cleartext, it will md5 it itself
139ebfdb 1071 $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
b9ddb2d5 1072 if (!$result) {
139ebfdb 1073 error_log('LDAP Error in user_update_password(). Error code: '
b9ddb2d5 1074 . ldap_errno($ldapconnection) . '; Error string : '
1075 . ldap_err2str(ldap_errno($ldapconnection)));
1076 }
139ebfdb 1077
b9ddb2d5 1078 }
1079
1080 @ldap_close($ldapconnection);
1081 return $result;
1082 }
1083
1084 //PRIVATE FUNCTIONS starts
1085 //private functions are named as ldap_*
1086
1087 /**
1088 * returns predefined usertypes
1089 *
1090 * @return array of predefined usertypes
1091 */
b9ddb2d5 1092 function ldap_suppported_usertypes() {
139ebfdb 1093 $types = array();
b9ddb2d5 1094 $types['edir']='Novell Edirectory';
1095 $types['rfc2307']='posixAccount (rfc2307)';
1096 $types['rfc2307bis']='posixAccount (rfc2307bis)';
1097 $types['samba']='sambaSamAccount (v.3.0.7)';
139ebfdb 1098 $types['ad']='MS ActiveDirectory';
1099 $types['default']=get_string('default');
b9ddb2d5 1100 return $types;
139ebfdb 1101 }
1102
b9ddb2d5 1103
b9ddb2d5 1104 /**
139ebfdb 1105 * Initializes needed variables for ldap-module
b9ddb2d5 1106 *
1107 * Uses names defined in ldap_supported_usertypes.
1108 * $default is first defined as:
1109 * $default['pseudoname'] = array(
1110 * 'typename1' => 'value',
1111 * 'typename2' => 'value'
1112 * ....
1113 * );
1114 *
1115 * @return array of default values
1116 */
1117 function ldap_getdefaults() {
1118 $default['objectclass'] = array(
1119 'edir' => 'User',
139ebfdb 1120 'rfc2307' => 'posixAccount',
1121 'rfc2307bis' => 'posixAccount',
b9ddb2d5 1122 'samba' => 'sambaSamAccount',
1123 'ad' => 'user',
1124 'default' => '*'
1125 );
1126 $default['user_attribute'] = array(
1127 'edir' => 'cn',
1128 'rfc2307' => 'uid',
1129 'rfc2307bis' => 'uid',
1130 'samba' => 'uid',
1131 'ad' => 'cn',
1132 'default' => 'cn'
1133 );
1134 $default['memberattribute'] = array(
1135 'edir' => 'member',
1136 'rfc2307' => 'member',
1137 'rfc2307bis' => 'member',
1138 'samba' => 'member',
139ebfdb 1139 'ad' => 'member',
b9ddb2d5 1140 'default' => 'member'
1141 );
1142 $default['memberattribute_isdn'] = array(
1143 'edir' => '1',
1144 'rfc2307' => '0',
1145 'rfc2307bis' => '1',
1146 'samba' => '0', //is this right?
1147 'ad' => '1',
1148 'default' => '0'
1149 );
1150 $default['expireattr'] = array (
1151 'edir' => 'passwordExpirationTime',
1152 'rfc2307' => 'shadowExpire',
1153 'rfc2307bis' => 'shadowExpire',
1154 'samba' => '', //No support yet
1155 'ad' => '', //No support yet
1156 'default' => ''
1157 );
139ebfdb 1158 return $default;
b9ddb2d5 1159 }
1160
1161 /**
1162 * return binaryfields of selected usertype
1163 *
1164 *
1165 * @return array
1166 */
1167 function ldap_getbinaryfields () {
b9ddb2d5 1168 $binaryfields = array (
1169 'edir' => array('guid'),
139ebfdb 1170 'rfc2307' => array(),
1171 'rfc2307bis' => array(),
b9ddb2d5 1172 'samba' => array(),
1173 'ad' => array(),
139ebfdb 1174 'default' => array()
b9ddb2d5 1175 );
1176 if (!empty($this->config->user_type)) {
139ebfdb 1177 return $binaryfields[$this->config->user_type];
b9ddb2d5 1178 }
1179 else {
1180 return $binaryfields['default'];
139ebfdb 1181 }
b9ddb2d5 1182 }
1183
1184 function ldap_isbinary ($field) {
139ebfdb 1185 if (empty($field)) {
1186 return false;
b9ddb2d5 1187 }
139ebfdb 1188 return array_search($field, $this->ldap_getbinaryfields());
b9ddb2d5 1189 }
1190
1191 /**
1192 * take expirationtime and return it as unixseconds
139ebfdb 1193 *
b9ddb2d5 1194 * takes expriration timestamp as readed from ldap
1195 * returns it as unix seconds
139ebfdb 1196 * depends on $this->config->user_type variable
b9ddb2d5 1197 *
1198 * @param mixed time Time stamp readed from ldap as it is.
1199 * @return timestamp
1200 */
1201 function ldap_expirationtime2unix ($time) {
b9ddb2d5 1202 $result = false;
1203 switch ($this->config->user_type) {
1204 case 'edir':
1205 $yr=substr($time,0,4);
1206 $mo=substr($time,4,2);
1207 $dt=substr($time,6,2);
1208 $hr=substr($time,8,2);
1209 $min=substr($time,10,2);
1210 $sec=substr($time,12,2);
139ebfdb 1211 $result = mktime($hr,$min,$sec,$mo,$dt,$yr);
b9ddb2d5 1212 break;
1213 case 'posix':
1214 $result = $time * DAYSECS; //The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date
1215 break;
139ebfdb 1216 default:
e8b9d76a 1217 print_error('auth_ldap_usertypeundefined', 'auth');
b9ddb2d5 1218 }
1219 return $result;
1220 }
1221
1222 /**
1223 * takes unixtime and return it formated for storing in ldap
1224 *
1225 * @param integer unix time stamp
1226 */
1227 function ldap_unix2expirationtime($time) {
b9ddb2d5 1228 $result = false;
1229 switch ($this->config->user_type) {
1230 case 'edir':
139ebfdb 1231 $result=date('YmdHis', $time).'Z';
b9ddb2d5 1232 break;
1233 case 'posix':
1234 $result = $time ; //Already in correct format
1235 break;
139ebfdb 1236 default:
e8b9d76a 1237 print_error('auth_ldap_usertypeundefined2', 'auth');
139ebfdb 1238 }
b9ddb2d5 1239 return $result;
1240
1241 }
1242
139ebfdb 1243 /**
b9ddb2d5 1244 * checks if user belong to specific group(s)
1245 *
1246 * Returns true if user belongs group in grupdns string.
1247 *
1248 * @param mixed $username username
1249 * @param mixed $groupdns string of group dn separated by ;
1250 *
1251 */
139ebfdb 1252 function ldap_isgroupmember($extusername='', $groupdns='') {
b9ddb2d5 1253 // Takes username and groupdn(s) , separated by ;
1254 // Returns true if user is member of any given groups
1255
b9ddb2d5 1256 $ldapconnection = $this->ldap_connect();
139ebfdb 1257
cd874e21 1258 if (empty($extusername) or empty($groupdns)) {
1259 return false;
b9ddb2d5 1260 }
1261
1262 if ($this->config->memberattribute_isdn) {
cd874e21 1263 $memberuser = $this->ldap_find_userdn($ldapconnection, $extusername);
1264 } else {
1265 $memberuser = $extusername;
b9ddb2d5 1266 }
cd874e21 1267
1268 if (empty($memberuser)) {
1269 return false;
b9ddb2d5 1270 }
1271
1272 $groups = explode(";",$groupdns);
139ebfdb 1273
cd874e21 1274 $result = false;
b9ddb2d5 1275 foreach ($groups as $group) {
1276 $group = trim($group);
1277 if (empty($group)) {
1278 continue;
1279 }
1280 //echo "Checking group $group for member $username\n";
cd874e21 1281 $search = ldap_read($ldapconnection, $group, '('.$this->config->memberattribute.'='.$this->filter_addslashes($memberuser).')', array($this->config->memberattribute));
1282 if (!empty($search) and ldap_count_entries($ldapconnection, $search)) {
1283 $info = $this->ldap_get_entries($ldapconnection, $search);
139ebfdb 1284
b9ddb2d5 1285 if (count($info) > 0 ) {
1286 // user is member of group
1287 $result = true;
1288 break;
1289 }
cd874e21 1290 }
b9ddb2d5 1291 }
b9ddb2d5 1292
1293 return $result;
1294
1295 }
1296
1297 /**
1298 * connects to ldap server
1299 *
1300 * Tries connect to specified ldap servers.
1301 * Returns connection result or error.
1302 *
1303 * @return connection result
1304 */
1305 function ldap_connect($binddn='',$bindpwd='') {
b9ddb2d5 1306 //Select bind password, With empty values use
1307 //ldap_bind_* variables or anonymous bind if ldap_bind_* are empty
1308 if ($binddn == '' and $bindpwd == '') {
1309 if (!empty($this->config->bind_dn)) {
1310 $binddn = $this->config->bind_dn;
1311 }
1312 if (!empty($this->config->bind_pw)) {
1313 $bindpwd = $this->config->bind_pw;
1314 }
1315 }
139ebfdb 1316
b9ddb2d5 1317 $urls = explode(";",$this->config->host_url);
139ebfdb 1318
b9ddb2d5 1319 foreach ($urls as $server) {
1320 $server = trim($server);
1321 if (empty($server)) {
1322 continue;
1323 }
1324
1325 $connresult = ldap_connect($server);
1326 //ldap_connect returns ALWAYS true
139ebfdb 1327
b9ddb2d5 1328 if (!empty($this->config->version)) {
1329 ldap_set_option($connresult, LDAP_OPT_PROTOCOL_VERSION, $this->config->version);
1330 }
1331
1332 if (!empty($binddn)) {
1333 //bind with search-user
139ebfdb 1334 //$debuginfo .= 'Using bind user'.$binddn.'and password:'.$bindpwd;
b9ddb2d5 1335 $bindresult=ldap_bind($connresult, $binddn,$bindpwd);
1336 }
1337 else {
139ebfdb 1338 //bind anonymously
b9ddb2d5 1339 $bindresult=@ldap_bind($connresult);
139ebfdb 1340 }
1341
b9ddb2d5 1342 if (!empty($this->config->opt_deref)) {
1343 ldap_set_option($connresult, LDAP_OPT_DEREF, $this->config->opt_deref);
1344 }
1345
1346 if ($bindresult) {
1347 return $connresult;
1348 }
139ebfdb 1349
b9ddb2d5 1350 $debuginfo .= "<br/>Server: '$server' <br/> Connection: '$connresult'<br/> Bind result: '$bindresult'</br>";
1351 }
1352
1353 //If any of servers are alive we have already returned connection
e8b9d76a 1354 print_error('auth_ldap_noconnect_all','auth',$this->config->user_type);
b9ddb2d5 1355 return false;
1356 }
1357
1358 /**
1359 * retuns dn of username
1360 *
1361 * Search specified contexts for username and return user dn
1362 * like: cn=username,ou=suborg,o=org
1363 *
1364 * @param mixed $ldapconnection $ldapconnection result
139ebfdb 1365 * @param mixed $username username (external encoding no slashes)
b9ddb2d5 1366 *
1367 */
1368
139ebfdb 1369 function ldap_find_userdn ($ldapconnection, $extusername) {
b9ddb2d5 1370
1371 //default return value
1372 $ldap_user_dn = FALSE;
1373
1374 //get all contexts and look for first matching user
1375 $ldap_contexts = explode(";",$this->config->contexts);
139ebfdb 1376
b9ddb2d5 1377 if (!empty($this->config->create_context)) {
1378 array_push($ldap_contexts, $this->config->create_context);
1379 }
139ebfdb 1380
b9ddb2d5 1381 foreach ($ldap_contexts as $context) {
1382
1383 $context = trim($context);
1384 if (empty($context)) {
1385 continue;
1386 }
1387
1388 if ($this->config->search_sub) {
1389 //use ldap_search to find first user from subtree
139ebfdb 1390 $ldap_result = ldap_search($ldapconnection, $context, "(".$this->config->user_attribute."=".$this->filter_addslashes($extusername).")",array($this->config->user_attribute));
b9ddb2d5 1391
1392 }
1393 else {
1394 //search only in this context
139ebfdb 1395 $ldap_result = ldap_list($ldapconnection, $context, "(".$this->config->user_attribute."=".$this->filter_addslashes($extusername).")",array($this->config->user_attribute));
b9ddb2d5 1396 }
139ebfdb 1397
b9ddb2d5 1398 $entry = ldap_first_entry($ldapconnection,$ldap_result);
1399
1400 if ($entry) {
1401 $ldap_user_dn = ldap_get_dn($ldapconnection, $entry);
1402 break ;
1403 }
1404 }
1405
1406 return $ldap_user_dn;
1407 }
1408
1409 /**
1410 * retuns user attribute mappings between moodle and ldap
1411 *
1412 * @return array
1413 */
1414
1415 function ldap_attributes () {
139ebfdb 1416 $fields = array("firstname", "lastname", "email", "phone1", "phone2",
1417 "department", "address", "city", "country", "description",
b9ddb2d5 1418 "idnumber", "lang" );
1419 $moodleattributes = array();
1420 foreach ($fields as $field) {
1421 if (!empty($this->config->{"field_map_$field"})) {
1422 $moodleattributes[$field] = $this->config->{"field_map_$field"};
1423 if (preg_match('/,/',$moodleattributes[$field])) {
1424 $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ?
1425 }
1426 }
1427 }
1428 $moodleattributes['username'] = $this->config->user_attribute;
1429 return $moodleattributes;
1430 }
1431
1432 /**
1433 * return all usernames from ldap
1434 *
1435 * @return array
1436 */
1437
1438 function ldap_get_userlist($filter="*") {
1439 /// returns all users from ldap servers
b9ddb2d5 1440 $fresult = array();
1441
1442 $ldapconnection = $this->ldap_connect();
1443
1444 if ($filter=="*") {
1445 $filter = "(&(".$this->config->user_attribute."=*)(".$this->config->objectclass."))";
1446 }
1447
1448 $contexts = explode(";",$this->config->contexts);
139ebfdb 1449
b9ddb2d5 1450 if (!empty($this->config->create_context)) {
1451 array_push($contexts, $this->config->create_context);
1452 }
1453
1454 foreach ($contexts as $context) {
1455
1456 $context = trim($context);
1457 if (empty($context)) {
1458 continue;
1459 }
1460
1461 if ($this->config->search_sub) {
1462 //use ldap_search to find first user from subtree
1463 $ldap_result = ldap_search($ldapconnection, $context,$filter,array($this->config->user_attribute));
1464 }
1465 else {
1466 //search only in this context
1467 $ldap_result = ldap_list($ldapconnection, $context,
1468 $filter,
1469 array($this->config->user_attribute));
1470 }
139ebfdb 1471
b9ddb2d5 1472 $users = $this->ldap_get_entries($ldapconnection, $ldap_result);
1473
1474 //add found users to list
1475 for ($i=0;$i<count($users);$i++) {
1476 array_push($fresult, ($users[$i][$this->config->user_attribute][0]) );
1477 }
1478 }
139ebfdb 1479
b9ddb2d5 1480 return $fresult;
1481 }
1482
1483 /**
1484 * return entries from ldap
1485 *
1486 * Returns values like ldap_get_entries but is
1487 * binary compatible and return all attributes as array
1488 *
1489 * @return array ldap-entries
1490 */
139ebfdb 1491
b9ddb2d5 1492 function ldap_get_entries($conn, $searchresult) {
1493 //Returns values like ldap_get_entries but is
1494 //binary compatible
1495 $i=0;
1496 $fresult=array();
1497 $entry = ldap_first_entry($conn, $searchresult);
1498 do {
1499 $attributes = @ldap_get_attributes($conn, $entry);
1500 for ($j=0; $j<$attributes['count']; $j++) {
1501 $values = ldap_get_values_len($conn, $entry,$attributes[$j]);
1502 if (is_array($values)) {
1503 $fresult[$i][$attributes[$j]] = $values;
1504 }
1505 else {
1506 $fresult[$i][$attributes[$j]] = array($values);
1507 }
139ebfdb 1508 }
1509 $i++;
b9ddb2d5 1510 }
1511 while ($entry = @ldap_next_entry($conn, $entry));
1512 //were done
1513 return ($fresult);
1514 }
1515
1516 /**
1517 * Returns true if this authentication plugin is 'internal'.
1518 *
139ebfdb 1519 * @return bool
b9ddb2d5 1520 */
1521 function is_internal() {
1522 return false;
1523 }
1524
1525 /**
1526 * Returns true if this authentication plugin can change the user's
1527 * password.
1528 *
139ebfdb 1529 * @return bool
b9ddb2d5 1530 */
1531 function can_change_password() {
430759a5 1532 return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl);
b9ddb2d5 1533 }
139ebfdb 1534
b9ddb2d5 1535 /**
430759a5 1536 * Returns the URL for changing the user's pw, or empty if the default can
b9ddb2d5 1537 * be used.
1538 *
139ebfdb 1539 * @return string url
b9ddb2d5 1540 */
1541 function change_password_url() {
139ebfdb 1542 if (empty($this->config->stdchangepassword)) {
1543 return $this->config->changepasswordurl;
1544 } else {
430759a5 1545 return '';
139ebfdb 1546 }
b9ddb2d5 1547 }
139ebfdb 1548
6bc1e5d5 1549 /**
1550 * Sync roles for this user
1551 *
1552 * @param $user object user object (without system magic quotes)
1553 */
1554 function sync_roles($user) {
1555 $iscreator = $this->iscreator($user->username);
1556 if ($iscreator === null) {
1557 return; //nothing to sync - creators not configured
1558 }
1559
1560 if ($roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
1561 $creatorrole = array_shift($roles); // We can only use one, let's use the first one
1562 $systemcontext = get_context_instance(CONTEXT_SYSTEM);
1563
1564 if ($iscreator) { // Following calls will not create duplicates
1565 role_assign($creatorrole->id, $user->id, 0, $systemcontext->id, 0, 0, 0, 'ldap');
1566 } else {
1567 //unassign only if previously assigned by this plugin!
1568 role_unassign($creatorrole->id, $user->id, 0, $systemcontext->id, 'ldap');
1569 }
1570 }
1571 }
1572
b9ddb2d5 1573 /**
1574 * Prints a form for configuring this authentication plugin.
1575 *
1576 * This function is called from admin/auth.php, and outputs a full page with
1577 * a form for configuring this plugin.
1578 *
1579 * @param array $page An object containing all the data for this page.
1580 */
139ebfdb 1581 function config_form($config, $err, $user_fields) {
1582 include 'config.html';
b9ddb2d5 1583 }
1584
1585 /**
1586 * Processes and stores configuration data for this authentication plugin.
1587 */
1588 function process_config($config) {
1589 // set to defaults if undefined
139ebfdb 1590 if (!isset($config->host_url))
b9ddb2d5 1591 { $config->host_url = ''; }
139ebfdb 1592 if (empty($config->ldapencoding))
1593 { $config->ldapencoding = 'utf-8'; }
1594 if (!isset($config->contexts))
b9ddb2d5 1595 { $config->contexts = ''; }
139ebfdb 1596 if (!isset($config->user_type))
1597 { $config->user_type = 'default'; }
1598 if (!isset($config->user_attribute))
b9ddb2d5 1599 { $config->user_attribute = ''; }
139ebfdb 1600 if (!isset($config->search_sub))
b9ddb2d5 1601 { $config->search_sub = ''; }
139ebfdb 1602 if (!isset($config->opt_deref))
b9ddb2d5 1603 { $config->opt_deref = ''; }
139ebfdb 1604 if (!isset($config->preventpassindb))
1605 { $config->preventpassindb = 0; }
1606 if (!isset($config->bind_dn))
b9ddb2d5 1607 {$config->bind_dn = ''; }
139ebfdb 1608 if (!isset($config->bind_pw))
b9ddb2d5 1609 {$config->bind_pw = ''; }
139ebfdb 1610 if (!isset($config->version))
b9ddb2d5 1611 {$config->version = '2'; }
139ebfdb 1612 if (!isset($config->objectclass))
b9ddb2d5 1613 {$config->objectclass = ''; }
139ebfdb 1614 if (!isset($config->memberattribute))
b9ddb2d5 1615 {$config->memberattribute = ''; }
cd874e21 1616 if (!isset($config->memberattribute_isdn))
1617 {$config->memberattribute_isdn = ''; }
139ebfdb 1618 if (!isset($config->creators))
b9ddb2d5 1619 {$config->creators = ''; }
139ebfdb 1620 if (!isset($config->create_context))
b9ddb2d5 1621 {$config->create_context = ''; }
139ebfdb 1622 if (!isset($config->expiration))
b9ddb2d5 1623 {$config->expiration = ''; }
139ebfdb 1624 if (!isset($config->expiration_warning))
b9ddb2d5 1625 {$config->expiration_warning = '10'; }
139ebfdb 1626 if (!isset($config->expireattr))
b9ddb2d5 1627 {$config->expireattr = ''; }
139ebfdb 1628 if (!isset($config->gracelogins))
b9ddb2d5 1629 {$config->gracelogins = ''; }
139ebfdb 1630 if (!isset($config->graceattr))
b9ddb2d5 1631 {$config->graceattr = ''; }
139ebfdb 1632 if (!isset($config->auth_user_create))
b9ddb2d5 1633 {$config->auth_user_create = ''; }
139ebfdb 1634 if (!isset($config->forcechangepassword))
1635 {$config->forcechangepassword = 0; }
b9ddb2d5 1636 if (!isset($config->stdchangepassword))
344514fc 1637 {$config->forcechangepassword = 0; }
1638 if (!isset($config->passtype))
1639 {$config->passtype = 'plaintext'; }
b9ddb2d5 1640 if (!isset($config->changepasswordurl))
1641 {$config->changepasswordurl = ''; }
139ebfdb 1642 if (!isset($config->removeuser))
1643 {$config->removeuser = 0; }
b9ddb2d5 1644
1645 // save settings
1646 set_config('host_url', $config->host_url, 'auth/ldap');
139ebfdb 1647 set_config('ldapencoding', $config->ldapencoding, 'auth/ldap');
1648 set_config('host_url', $config->host_url, 'auth/ldap');
b9ddb2d5 1649 set_config('contexts', $config->contexts, 'auth/ldap');
1650 set_config('user_type', $config->user_type, 'auth/ldap');
1651 set_config('user_attribute', $config->user_attribute, 'auth/ldap');
1652 set_config('search_sub', $config->search_sub, 'auth/ldap');
1653 set_config('opt_deref', $config->opt_deref, 'auth/ldap');
1654 set_config('preventpassindb', $config->preventpassindb, 'auth/ldap');
1655 set_config('bind_dn', $config->bind_dn, 'auth/ldap');
1656 set_config('bind_pw', $config->bind_pw, 'auth/ldap');
1657 set_config('version', $config->version, 'auth/ldap');
1658 set_config('objectclass', $config->objectclass, 'auth/ldap');
1659 set_config('memberattribute', $config->memberattribute, 'auth/ldap');
cd874e21 1660 set_config('memberattribute_isdn', $config->memberattribute_isdn, 'auth/ldap');
b9ddb2d5 1661 set_config('creators', $config->creators, 'auth/ldap');
1662 set_config('create_context', $config->create_context, 'auth/ldap');
1663 set_config('expiration', $config->expiration, 'auth/ldap');
1664 set_config('expiration_warning', $config->expiration_warning, 'auth/ldap');
1665 set_config('expireattr', $config->expireattr, 'auth/ldap');
1666 set_config('gracelogins', $config->gracelogins, 'auth/ldap');
1667 set_config('graceattr', $config->graceattr, 'auth/ldap');
1668 set_config('auth_user_create', $config->auth_user_create, 'auth/ldap');
1669 set_config('forcechangepassword', $config->forcechangepassword, 'auth/ldap');
1670 set_config('stdchangepassword', $config->stdchangepassword, 'auth/ldap');
344514fc 1671 set_config('passtype', $config->passtype, 'auth/ldap');
b9ddb2d5 1672 set_config('changepasswordurl', $config->changepasswordurl, 'auth/ldap');
139ebfdb 1673 set_config('removeuser', $config->removeuser, 'auth/ldap');
b9ddb2d5 1674
1675 return true;
1676 }
1677
139ebfdb 1678 /**
1679 * Quote control characters in texts used in ldap filters - see rfc2254.txt
1680 *
1681 * @param string
1682 */
1683 function filter_addslashes($text) {
1684 $text = str_replace('\\', '\\5c', $text);
1685 $text = str_replace(array('*', '(', ')', "\0"),
1686 array('\\2a', '\\28', '\\29', '\\00'), $text);
1687 return $text;
1688 }
1689
1690 /**
1691 * Quote control characters in quoted "texts" used in ldap
1692 *
1693 * @param string
1694 */
1695 function ldap_addslashes($text) {
1696 $text = str_replace('\\', '\\\\', $text);
1697 $text = str_replace(array('"', "\0"),
1698 array('\\"', '\\00'), $text);
1699 return $text;
1700 }
b9ddb2d5 1701}
1702
1703?>