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