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