multiauth: migrated all files to the new OO API, written new API documentation
[moodle.git] / auth / db / 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: External Database Authentication
9 *
10 * Checks against an external database.
11 *
12 * 2006-08-28 File created.
13 */
14
15// This page cannot be called directly
16if (!isset($CFG)) exit;
17
18/**
19 * External database authentication plugin.
20 */
21class auth_plugin_db {
22
23 /**
24 * The configuration details for the plugin.
25 */
26 var $config;
27
28 /**
29 * Constructor.
30 */
31 function auth_plugin_db() {
32 $this->config = get_config('auth/db');
33 }
34
35 /**
36 * Returns true if the username and password work and false if they are
37 * wrong or don't exist.
38 *
39 * @param string $username The username
40 * @param string $password The password
41 * @returns bool Authentication success or failure.
42 */
43 function user_login ($username, $password) {
44
45 global $CFG;
46
47 // This is a hack to workaround what seems to be a bug in ADOdb with accessing
48 // two databases of the same kind ... it seems to get confused when trying to access
49 // the first database again, after having accessed the second.
50 // The following hack will make the database explicit which keeps it happy
51 // This seems to broke postgesql so ..
52
53 $prefix = $CFG->prefix.''; // Remember it. The '' is to prevent PHP5 reference.. see bug 3223
54
55 if ($CFG->dbtype != 'postgres7') {
56 $CFG->prefix = $CFG->dbname.$CFG->prefix;
57 }
58
59 // Connect to the external database
60 $authdb = &ADONewConnection($this->config->type);
61 $authdb->PConnect($this->config->host, $this->config->user, $this->config->pass, $this->config->name);
62 $authdb->SetFetchMode(ADODB_FETCH_ASSOC);
63
64 if ($this->config->passtype === 'internal') {
65 // lookup username externally, but resolve
66 // password locally -- to support backend that
67 // don't track passwords
68 $rs = $authdb->Execute("SELECT * FROM {$this->config->table}
69 WHERE {$this->config->fielduser} = '$username' ");
70 $authdb->Close();
71
72 if (!$rs) {
73 notify("Could not connect to the specified authentication database...");
74
75 return false;
76 }
77
78 if ( $rs->RecordCount() ) {
79 // user exists exterally
80 // check username/password internally
81 if ($user = get_record('user', 'username', $username)) {
82 return validate_internal_user_password($user, $password);
83 }
84 } else {
85 // user does not exist externally
86 return false;
87 }
88
89 } else {
90 // normal case: use external db for passwords
91
92 if ($this->config->passtype === 'md5') { // Re-format password accordingly
93 $password = md5($password);
94 }
95
96 $rs = $authdb->Execute("SELECT * FROM {$this->config->table}
97 WHERE {$this->config->fielduser} = '$username'
98 AND {$this->config->fieldpass} = '$password' ");
99 $authdb->Close();
100
101 $CFG->prefix = $prefix;
102
103 if (!$rs) {
104 notify("Could not connect to the specified authentication database...");
105 return false;
106 }
107
108 if ( $rs->RecordCount() ) {
109 return true;
110 } else {
111 return false;
112 }
113
114 }
115 }
116
117
118 /**
119 * Reads any other information for a user from external database,
120 * then returns it in an array
121 */
122 function get_userinfo($username) {
123
124 global $CFG;
125
126 ADOLoadCode($this->config->type);
127 $authdb = &ADONewConnection();
128 $authdb->PConnect($this->config->host, $this->config->user, $this->config->pass, $this->config->name);
129 $authdb->SetFetchMode(ADODB_FETCH_ASSOC);
130
131 $fields = array("firstname", "lastname", "email", "phone1", "phone2",
132 "department", "address", "city", "country", "description",
133 "idnumber", "lang");
134
135 $result = array();
136
137 foreach ($fields as $field) {
138 if ($this->config->{'field_map_' . $field}) {
139 if ($rs = $authdb->Execute("SELECT " . $this->config->{'field_map_' . $field} . " FROM {$this->config->table}
140 WHERE {$this->config->fielduser} = '$username'")) {
141 if ( $rs->RecordCount() == 1 ) {
142 if (!empty($CFG->unicodedb)) {
143 $result["$field"] = addslashes(stripslashes($rs->fields[0]));
144 } else {
145 $result["$field"] = addslashes(stripslashes(utf8_decode($rs->fields[0])));
146 }
147 }
148 }
149 }
150 }
151 $authdb->Close();
152
153 return $result;
154 }
155
156
157 function user_update_password($username, $newpassword) {
158
159 if ($this->config->passtype === 'internal') {
160 return set_field('user', 'password', md5($newpassword), 'username', $username);
161 } else {
162 // we should have never been called!
163 return false;
164 }
165 }
166
167 /**
168 * syncronizes user fron external db to moodle user table
169 *
170 * Sync shouid be done by using idnumber attribute, not username.
171 * You need to pass firstsync parameter to function to fill in
172 * idnumbers if they dont exists in moodle user table.
173 *
174 * Syncing users removes (disables) users that dont exists anymore in external db.
175 * Creates new users and updates coursecreator status of users.
176 *
177 * @param bool $do_updates Optional: set to true to force an update of existing accounts
178 *
179 * This implementation is simpler but less scalable than the one found in the LDAP module.
180 *
181 */
182 function sync_users ($do_updates=0) {
183
184 global $CFG;
185 $pcfg = get_config('auth/db');
186
187 ///
188 /// list external users
189 ///
190 $userlist = $this->get_userlist();
191 $quoteduserlist = implode("', '", $userlist);
192 $quoteduserlist = "'$quoteduserlist'";
193
194 ///
195 /// delete obsolete internal users
196 ///
197
198 // find obsolete users
199 if (count($userlist)) {
200 $sql = 'SELECT u.id, u.username
201 FROM ' . $CFG->prefix .'user u
202 WHERE u.auth=\'db\' AND u.deleted=\'0\' AND u.username NOT IN (' . $quoteduserlist . ')';
203 } else {
204 $sql = 'SELECT u.id, u.username
205 FROM ' . $CFG->prefix .'user u
206 WHERE u.auth=\'db\' AND u.deleted=\'0\' ';
207 }
208 $remove_users = get_records_sql($sql);
209
210 if (!empty($remove_users)) {
211 print "User entries to remove: ". count($remove_users) . "\n";
212
213 begin_sql();
214 foreach ($remove_users as $user) {
215 //following is copy pasted from admin/user.php
216 //maybe this should moved to function in lib/datalib.php
217 unset($updateuser);
218 $updateuser->id = $user->id;
219 $updateuser->deleted = "1";
220 $updateuser->timemodified = time();
221 if (update_record("user", $updateuser)) {
222 // unenrol_student($user->id); // From all courses
223 // remove_teacher($user->id); // From all courses
224 // remove_admin($user->id);
225 delete_records('role_assignments', 'userid', $user->id); // unassign all roles
226 notify(get_string("deletedactivity", "", fullname($user, true)) );
227 } else {
228 notify(get_string("deletednot", "", fullname($user, true)));
229 }
230 //copy pasted part ends
231 }
232 commit_sql();
233 }
234 unset($remove_users); // free mem!
235
236 if (!count($userlist)) {
237 // exit right here
238 // nothing else to do
239 return true;
240 }
241
242 ///
243 /// update existing accounts
244 ///
245 if ($do_updates) {
246 // narrow down what fields we need to update
247 $all_keys = array_keys(get_object_vars($this->config));
248 $updatekeys = array();
249 foreach ($all_keys as $key) {
250 if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) {
251 if ($this->config->{$key} === 'onlogin') {
252 array_push($updatekeys, $match[1]); // the actual key name
253 }
254 }
255 }
256 // print_r($all_keys); print_r($updatekeys);
257 unset($all_keys); unset($key);
258
259 // only go ahead if we actually
260 // have fields to update locally
261 if (!empty($updatekeys)) {
262 $sql = 'SELECT u.id, u.username
263 FROM ' . $CFG->prefix .'user u
264 WHERE u.auth=\'db\' AND u.deleted=\'0\' AND u.username IN (' . $quoteduserlist . ')';
265 $update_users = get_records_sql($sql);
266
267 foreach ($update_users as $user) {
268 $this->db_update_user_record($user->username, $updatekeys);
269 }
270 unset($update_users); // free memory
271 }
272 }
273
274
275 ///
276 /// create missing accounts
277 ///
278 // NOTE: this is very memory intensive
279 // and generally inefficient
280 $sql = 'SELECT u.id, u.username
281 FROM ' . $CFG->prefix .'user u
282 WHERE u.auth=\'db\' AND u.deleted=\'0\'';
283
284 $users = get_records_sql($sql);
285
286 // simplify down to usernames
287 $usernames = array();
288 foreach ($users as $user) {
289 array_push($usernames, $user->username);
290 }
291 unset($users);
292
293 $add_users = array_diff($userlist, $usernames);
294 unset($usernames);
295
296 if (!empty($add_users)) {
297 print "User entries to add: ". count($add_users). "\n";
298 begin_sql();
299 foreach($add_users as $user) {
300 $username = $user;
301 $user = $this->get_userinfo_asobj($user);
302
303 // prep a few params
304 $user->username = $username;
305 $user->modified = time();
306 $user->confirmed = 1;
307 $user->auth = 'db';
308
309 // insert it
310 $old_debug=$CFG->debug;
311 $CFG->debug=10;
312
313 // maybe the user has been deleted before
314 if ($old_user = get_record('user', 'username', $user->username, 'deleted', 1)) {
315 $user->id = $old_user->id;
316 set_field('user', 'deleted', 0, 'username', $user->username);
317 echo "Revived user $user->username id $user->id\n";
318 } elseif ($id=insert_record ('user',$user)) { // it is truly a new user
319 echo "inserted user $user->username id $id\n";
320 $user->id = $id;
321 // if relevant, tag for password generation
322 if ($this->config->passtype === 'internal') {
323 set_user_preference('auth_forcepasswordchange', 1, $id);
324 set_user_preference('create_password', 1, $id);
325 }
326 } else {
327 echo "error inserting user $user->username \n";
328 }
329 $CFG->debug=$old_debug;
330 }
331 commit_sql();
332 unset($add_users); // free mem
333 }
334 return true;
335 }
336
337 function user_exists ($username) {
338 $authdb = &ADONewConnection($this->config->type);
339 $authdb->PConnect($this->config->host, $this->config->user, $this->config->pass, $this->config->name);
340 $authdb->SetFetchMode(ADODB_FETCH_ASSOC);
341
342 $rs = $authdb->Execute("SELECT * FROM {$this->config->table}
343 WHERE {$this->config->fielduser} = '$username' ");
344 $authdb->Close();
345
346 if (!$rs) {
347 notify("Could not connect to the specified authentication database...");
348 return false;
349 }
350
351 if ( $rs->RecordCount() ) {
352 // user exists exterally
353 // check username/password internally
354 // ?? there is no $password variable, so why??
355 /*if ($user = get_record('user', 'username', $username)) {
356 return ($user->password == md5($password));
357 }*/
358 return $rs->RecordCount();
359 } else {
360 // user does not exist externally
361 return false;
362 }
363 }
364
365
366 function get_userlist() {
367 // Connect to the external database
368 $authdb = &ADONewConnection($this->config->type);
369 $authdb->PConnect($this->config->host,$this->config->user,$this->config->pass,$this->config->name);
370 $authdb->SetFetchMode(ADODB_FETCH_ASSOC);
371
372 // fetch userlist
373 $rs = $authdb->Execute("SELECT {$this->config->fielduser} AS username
374 FROM {$this->config->table} ");
375 $authdb->Close();
376
377 if (!$rs) {
378 notify("Could not connect to the specified authentication database...");
379 return false;
380 }
381
382 if ( $rs->RecordCount() ) {
383 $userlist = array();
384 while ($rec = $rs->FetchRow()) {
385 array_push($userlist, $rec['username']);
386 }
387 return $userlist;
388 } else {
389 return array();
390 }
391 }
392
393 /**
394 * reads userinformation from DB and return it in an object
395 *
396 * @param string $username username
397 * @return array
398 */
399 function get_userinfo_asobj($username) {
400 $user_array = truncate_userinfo($this->get_userinfo($username));
401 $user = new object;
402 foreach($user_array as $key=>$value) {
403 $user->{$key} = $value;
404 }
405 return $user;
406 }
407
408 /*
409 * will update a local user record from an external source.
410 * is a lighter version of the one in moodlelib -- won't do
411 * expensive ops such as enrolment
412 *
413 * If you don't pass $updatekeys, there is a performance hit and
414 * values removed from DB won't be removed from moodle.
415 */
416 function db_update_user_record($username, $updatekeys=false) {
417
418 $pcfg = get_config('auth/db');
419
420 //just in case check text case
421 $username = trim(moodle_strtolower($username));
422
423 // get the current user record
424 $user = get_record('user', 'username', $username);
425 if (empty($user)) { // trouble
426 error_log("Cannot update non-existent user: $username");
427 die;
428 }
429
430 // TODO: this had a function_exists() - now we have a $this
431 if ($newinfo = $this->get_userinfo($username)) {
432 $newinfo = truncate_userinfo($newinfo);
433
434 if (empty($updatekeys)) { // all keys? this does not support removing values
435 $updatekeys = array_keys($newinfo);
436 }
437
438 foreach ($updatekeys as $key) {
439 unset($value);
440 if (isset($newinfo[$key])) {
441 $value = $newinfo[$key];
442 $value = addslashes(stripslashes($value)); // Just in case
443 } else {
444 $value = '';
445 }
446 if (!empty($this->config->{'field_updatelocal_' . $key})) {
447 if ($user->{$key} != $value) { // only update if it's changed
448 set_field('user', $key, $value, 'username', $username);
449 }
450 }
451 }
452 }
453 return get_record_select("user", "username = '$username' AND deleted <> '1'");
454 }
455
456 // A chance to validate form data, and last chance to
457 // do stuff before it is inserted in config_plugin
458 function validate_form(&$form, &$err) {
459 if ($form['passtype'] === 'internal') {
460 $this->config->changepasswordurl = '';
461 set_config('changepasswordurl', '', 'auth/db');
462 }
463 return true;
464 }
465
466 /**
467 * Returns true if this authentication plugin is 'internal'.
468 *
469 * @returns bool
470 */
471 function is_internal() {
472 return false;
473 }
474
475 /**
476 * Returns true if this authentication plugin can change the user's
477 * password.
478 *
479 * @returns bool
480 */
481 function can_change_password() {
482 return ($this->config->passtype === 'internal');
483 }
484
485 /**
486 * Returns the URL for changing the user's pw, or false if the default can
487 * be used.
488 *
489 * @returns bool
490 */
491 function change_password_url() {
492 return $this->config->changepasswordurl;
493 }
494
495 /**
496 * Prints a form for configuring this authentication plugin.
497 *
498 * This function is called from admin/auth.php, and outputs a full page with
499 * a form for configuring this plugin.
500 *
501 * @param array $page An object containing all the data for this page.
502 */
503 function config_form($config, $err) {
504 include "config.html";
505 }
506
507 /**
508 * Processes and stores configuration data for this authentication plugin.
509 */
510 function process_config($config) {
511 // set to defaults if undefined
512 if (!isset($config->host)) {
513 $config->host = "localhost";
514 }
515 if (!isset($config->type)) {
516 $config->type = "mysql";
517 }
518 if (!isset($config->name)) {
519 $config->name = "";
520 }
521 if (!isset($config->user)) {
522 $config->user = "";
523 }
524 if (!isset($config->pass)) {
525 $config->pass = "";
526 }
527 if (!isset($config->table)) {
528 $config->table = "";
529 }
530 if (!isset($config->fielduser)) {
531 $config->fielduser = "";
532 }
533 if (!isset($config->fieldpass)) {
534 $config->fieldpass = "";
535 }
536 if (!isset($config->passtype)) {
537 $config->passtype = "plaintext";
538 }
539 if (!isset($config->changepasswordurl)) {
540 $config->changepasswordurl = '';
541 }
542
543 // save settings
544 set_config('host', $config->host, 'auth/db');
545 set_config('type', $config->type, 'auth/db');
546 set_config('name', $config->name, 'auth/db');
547 set_config('user', $config->user, 'auth/db');
548 set_config('pass', $config->pass, 'auth/db');
549 set_config('table', $config->table, 'auth/db');
550 set_config('fielduser', $config->fielduser, 'auth/db');
551 set_config('fieldpass', $config->fieldpass, 'auth/db');
552 set_config('passtype', $config->passtype, 'auth/db');
553 set_config('changepasswordurl', $config->changepasswordurl, 'auth/db');
554
555 return true;
556 }
557
558}
559
560?>