From: martinlanghoff Date: Mon, 22 Nov 2004 07:46:10 +0000 (+0000) Subject: Auth/LDAP X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=b36a8fc4f5fe613eaba0f8dd670c9ce1bb52d79e;p=moodle.git Auth/LDAP Bugfix - value truncation to fit Moodle database - Added truncate_userinfo() to cleanup data coming from external auth - Fixed auth_user_create() to truncate user info as appropriate Auth_ldap_user_sync - created external script that calls the function - much faster update strategy on postgres and mysql: auth_sync_users now to uses bulk inserts into a temp table, and then use LEFT JOINs and plain old SELECTs to determine what users it has to insert. - we now loop over smaller sets of data -- we are still memory-bound, but (a) it'll be easy to use LIMIT to manage that and (b) memory use is much lower now in all cases. - postgres: phased commits in auth_user_sync() for the batch user upload phase - Several feature and performance enhancements: - if a value is removed from ldap, it will be cleared from moodle - no-op updates (where the data does not change) are skipped - if a user disappears and then reappears in LDAP in two separate calls to auth_user_sync(),the account will be marked deleted and then be revived. before, the account would have been deleted and created anew. Multi-source ldap values: The LDAP auth module now accepts a comma separated set of LDAP field names. When creating or updating a user record, auth/ldap will retrieve all the relevant fields. The right-most values overwrites all the others. This is particularly useful when updating the user's email address from an LDAP source, which may contain the email address in one of several fields (traditionally: mail, mailForwardingAddress, mailAlternateAddress). If a value is updated and is set to update external auth and this field is using this multi-source ldap configuration, the auth/ldap module will retrieve the old value, find which field it was sourced from, and update that field in LDAP. If it fails to find the original source of the value, it will log it in error_log. Log of patchsets applied: arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-131 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-137 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-139 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-172 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-173 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-189 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-190 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-208 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-212 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-216 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-279 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-282 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-287 arch-eduforge@catalyst.net.nz--2004/moodle--eduforge--1.3.3--patch-294 --- diff --git a/auth/ldap/lib.php b/auth/ldap/lib.php index 8a2005c0bf..afb1009e80 100644 --- a/auth/ldap/lib.php +++ b/auth/ldap/lib.php @@ -36,7 +36,7 @@ * @param string $username * @param string $password * - */ +*/ function auth_user_login ($username, $password) { global $CFG; @@ -45,7 +45,7 @@ function auth_user_login ($username, $password) { return false; } - $ldapconnection = auth_ldap_connect(); + $ldapconnection = auth_ldap_connect(); if ($ldapconnection) { $ldap_user_dn = auth_ldap_find_userdn($ldapconnection, $username); @@ -84,25 +84,39 @@ function auth_get_userinfo($username){ $ldapconnection=auth_ldap_connect(); $config = (array)$CFG; $attrmap = auth_ldap_attributes(); - + $result = array(); $search_attribs = array(); - foreach ($attrmap as $key=>$value) { - if (!in_array($value, $search_attribs)) { - array_push($search_attribs, $value); - } + foreach ($attrmap as $key=>$values) { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $value) { + if (!in_array($value, $search_attribs)) { + array_push($search_attribs, $value); + } + } } $user_dn = auth_ldap_find_userdn($ldapconnection, $username); + if (empty($CFG->ldap_objectclass)) { // Can't send empty filter + $CFG->ldap_objectclass="objectClass=*"; + } + $user_info_result = ldap_read($ldapconnection,$user_dn,$CFG->ldap_objectclass, $search_attribs); if ($user_info_result) { - $user_entry = auth_ldap_get_entries($ldapconnection, $user_info_result); - foreach ($attrmap as $key=>$value){ - if(isset($user_entry[0][strtolower($value)][0])){ - $result[$key]=utf8_decode($user_entry[0][strtolower($value)][0]); + $user_entry = ldap_get_entries($ldapconnection, $user_info_result); + foreach ($attrmap as $key=>$values){ + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $value) { + if(isset($user_entry[0][strtolower($value)][0])){ + $result[$key]=addslashes(stripslashes(utf8_decode($user_entry[0][strtolower($value)][0]))); + } } } } @@ -112,6 +126,21 @@ function auth_get_userinfo($username){ return $result; } +/** + * reads userinformation from ldap and return it in an object + * + * @param string $username username + * @return array + */ +function auth_get_userinfo_asobj($username){ + $user_array = truncate_userinfo(auth_get_userinfo($username)); + $user = new object; + foreach($user_array as $key=>$value){ + $user->{$key} = $value; + } + return $user; +} + /** * returns all usernames from external database * @@ -152,12 +181,15 @@ function auth_user_create ($userobject,$plainpass) { $newuser = array(); - foreach ($attrmap as $key=>$value){ + foreach ($attrmap as $key=>$values){ + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $value) { if(!empty($userobject->$key) ){ - if (isset($CFG->{auth_user_.$key._updateremote}) && $CFG->{auth_user_.$key._updateremote} == "1" ) { - $newuser[$value]=utf8_encode($userobject->$key); - } + $newuser[$value]=utf8_encode($userobject->$key); } + } } //Following sets all mandatory and other forced attribute values @@ -167,14 +199,14 @@ function auth_user_create ($userobject,$plainpass) { switch ($CFG->ldap_user_type) { case 'edir': - $newuser['objectClass']= array("inetOrgPerson","organizationalPerson","person","top"); - $newuser['uniqueId']= $userobject->username; - $newuser['logindisabled']="TRUE"; - $newuser['userpassword']=$plainpass; + $newuser['objectClass']= array("inetOrgPerson","organizationalPerson","person","top"); + $newuser['uniqueId']= $userobject->username; + $newuser['logindisabled']="TRUE"; + $newuser['userpassword']=$plainpass; default: error('auth: ldap auth_user_create() does not support selected usertype (..yet)'); } - + $uadd = ldap_add($ldapconnection, $CFG->ldap_user_attribute."=$userobject->username,".$CFG->ldap_create_context, $newuser); ldap_close($ldapconnection); @@ -212,10 +244,15 @@ function auth_get_users($filter='*', $dontlistcreated=false) { $search_attribs = array(); - foreach ($attrmap as $key=>$value) { - if (!in_array($value, $search_attribs)) { - array_push($search_attribs, $value); - } + foreach ($attrmap as $key=>$values) { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $value) { + if (!in_array($value, $search_attribs)) { + array_push($search_attribs, $value); + } + } } @@ -243,6 +280,8 @@ function auth_get_users($filter='*', $dontlistcreated=false) { $user->$key=$users[$ldapuser][$value][0]; } } + //quick way to get around binarystrings + $user->guid=bin2hex($user->guid); //add authentication source stamp $user->auth='ldap'; $fresult[$user->username]=$user; @@ -305,284 +344,334 @@ function auth_password_expire($username) { * * @param mixed $firstsync Optional: set to true to fill idnumber fields if not filled yet */ -function auth_sync_users ($firstsync=0, $unsafe_optimizations = false, $bulk_insert_records = 1) { +function auth_sync_users ($bulk_insert_records = 1000, $do_updates=1) { //Syncronizes userdb with ldap //This will add, rename /// OPTIONAL PARAMETERS -/// $unsafe_optimizations = true // will skip over moodle standard DB interfaces and use very optimized -//// and non-portable SQL -- useful only for mysql or postgres7 /// $bulk_insert_records = 1 // will insert $bulkinsert_records per insert statement /// valid only with $unsafe. increase to a couple thousand for /// blinding fast inserts -- but test it: you may hit mysqld's /// max_allowed_packet limit. +/// $do_updates = 1 // will do pull in data updates from ldap if relevant - global $CFG,$db ; - auth_ldap_init(); - $ldapusers = auth_get_users('*', true); //list all but created users from ldap - - if (!$unsafe_optimizations) { - //NORMAL SYNRONIZATION ROUTINE STARTS - $db->SetFetchMode(ADODB_FETCH_ASSOC) ; - if ($firstsync) { - //fill idnumbers - $users = get_records(user, 'auth', 'ldap', '', 'id, username', ''); - //to be continued later....... - foreach ( $ldapusers as $user) { - if (record_exists('user', 'auth', 'ldap', 'username', $user->username)) { - if (isset($user->idnumber)) { - //edir uses binary guid field - if ($CFG->ldap_user_type == 'edir'){ - $user->idnumber = bin2hex($user->idnumber); - } - $idnumber = $user->$CFG->idnumber; - - if (set_field('user', 'idnumber', $user->idnumber, 'username', $user->username, 'auth', 'ldap' )) { - echo 'Filled idnumber for user '.$user->username."\n"; - } else { - echo 'Cannot fill idnumber for user ' .$user->username. 'Problem updating database..'."\n"; - } - } else { - echo 'Cannot fill idnumber for user '.$user->username.'idnumber does not exist in ldap.'."\n"; - print_r($user); - } - } - } - echo "Firstsync complete!\n"; - return; - } - - //Build existing userinformation - $users = get_records(user, 'auth', 'ldap'); //get all user objects from db - - //Create array where idnumbers are keys - //make information available with idnumber - $moodleldapusers = array(); //all users in moodle db - foreach ($users as $id=>$info){ - $moodleldapusers[$info->idnumber]= $info; - $moodleldapusers[$info->idnumber]->id=$id; - } + global $CFG ; - unset($users); //not needed anymore - - //get attributemappings - $attrmap = auth_ldap_attributes(); - $updatelocals = array('username'); - $updateremotes = array(); - foreach ($attrmap as $key=>$value) { - if (isset($CFG->{'auth_user_'.$key.'_updatelocal'}) && $CFG->{'auth_user_'.$key.'_updatelocal'} ) { - $updatelocals[]=$key; - } + // configure a temp table + print "Configuring temp table\n"; + if(strtolower($CFG->dbtype) === 'mysql'){ + // help old mysql versions cope with large temp tables + execute_sql('SET SQL_BIG_TABLES=1', false); + execute_sql('CREATE TEMPORARY TABLE ' . $CFG->prefix .'extuser (idnumber VARCHAR(12), PRIMARY KEY (idnumber)) TYPE=MyISAM',false); + } elseif (strtolower($CFG->dbtype) === 'postgres7'){ + $bulk_insert_records = 1; // no support for multiple sets of values + execute_sql('CREATE TEMPORARY TABLE '.$CFG->prefix.'extuser (idnumber VARCHAR(12), PRIMARY KEY (idnumber))',false); + } - if (isset($CFG->{'auth_user_'.$key.'_updateremote'}) && $CFG->{'auth_user_'.$key.'_updateremote'} ) { - $updateremotes[]=$key; - } - } - $idattribute=$attrmap['idnumber']; - $updateusers = array(); //users what should be updated - $newusers = array(); //new users to create - //put userinformation in $updateusers and $newusers - foreach ($ldapusers as $user) { - if (!empty($user->idnumber)) { - //edir uses binary guid field - if ($CFG->ldap_user_type == 'edir'){ - $user->idnumber = bin2hex($user->idnumber); - } - $idnumber = $user->idnumber; - - if (array_key_exists($idnumber, $moodleldapusers)) { - $arraytoupdate = &$updateusers; - $userinfo = $moodleldapusers[$idnumber]; - unset($moodleldapusers[$idnumber]); - } else { - $arraytoupdate = &$newusers; - $userinfo = new object(); - $userinfo->idnumber = $idnumber; - } - - //update local userinformation - foreach ($updatelocals as $value) { - if (isset($user->$value)) { - $userinfo->$value = $user->$value; - } - } - //Force values for some fields - $userinfo->timemodified = time(); - $userinfo->confirmed = 1; - $userinfo->deleted = 0; - $userinfo->auth = 'ldap'; - - //store userinfo to selected array - $arraytoupdate[$idnumber] = $userinfo; - - } else { - //cannot sync remoteuser without idumber - echo 'Cannot sync remote user to moodle '.$user->dn." without idnumber field\n"; - } - } - - //update local users - foreach ($updateusers as $user) { + print "connecting to ldap\n"; + $ldapconnection = auth_ldap_connect(); - if (update_record('user', $user)) { - echo 'Updated user record '.$user->username."\n"; - } else { - echo 'Cannot update user record '.$user->username."\n"; - print_r($user); - } - } + //// + //// get user's list from ldap to sql in a scalable fashion + //// + // prepare some data we'll need + if (! empty($CFG->ldap_objectclass)) { + $CFG->ldap_objectclass="objectClass=*"; + } - //add new users - foreach ($newusers as $user) { - if (insert_record('user', $user, $false)) { - echo 'Inserted user record'.$user->username."\n"; - } else { - echo 'Cannot insert user record'.$user->username."\n"; - print_r($user); - } - } + $filter = "(&(".$CFG->ldap_user_attribute."=*)(".$CFG->ldap_objectclass."))"; - //remove old users - foreach ($moodleldapusers as $user) { - //following is copy pasted from admin/user.php - //maybe this should moved to function in lib/datalib.php - unset($remove); - $remove->id = $user->id; - $remove->deleted = "1"; - $remove->username = "$user->email.".time(); // Remember it just in case - $remove->email = ""; // Clear this field to free it up - $remove->timemodified = time(); - if (update_record("user", $remove)) { - unenrol_student($user->id); // From all courses - remove_teacher($user->id); // From all courses - remove_admin($user->id); - notify(get_string("deletedactivity", "", fullname($user, true)) ); - } else { - notify(get_string("deletednot", "", fullname($user, true))); - } - //copy pasted part ends - } + $contexts = explode(";",$CFG->ldap_contexts); + + if (!empty($CFG->ldap_create_context)){ + array_push($contexts, $CFG->ldap_create_context); + } - //NORMAL SYNRONIZATION ROUTINE ENDS - } else { - //UNSAFE OPTIMIZATIONS STARTS - $usedidnumbers = Array(); + $fresult = array(); + $count = 0; + foreach ($contexts as $context) { + begin_sql(); + if ($CFG->ldap_search_sub) { + //use ldap_search to find first user from subtree + $ldap_result = ldap_search($ldapconnection, $context, + $filter, + array($CFG->ldap_user_attribute)); + } else { + //search only in this context + $ldap_result = ldap_list($ldapconnection, $context, + $filter, + array($CFG->ldap_user_attribute)); + } - // these are only populated if we managed to find added/removed users - $add_users = false; - $remove_users = false; - - // create a temp table - if(strtolower($CFG->dbtype) === 'mysql'){ - // help old mysql versions cope with large temp tables - execute_sql('SET SQL_BIG_TABLES=1'); - execute_sql('CREATE TEMPORARY TABLE ' . $CFG->prefix .'extuser (idnumber VARCHAR(12), PRIMARY KEY (idnumber)) TYPE=MyISAM'); - } elseif (strtolower($CFG->dbtype) === 'postgres7'){ - execute_sql('CREATE TEMPORARY TABLE '.$CFG->prefix.'extuser (idnumber VARCHAR(12), PRIMARY KEY (idnumber))'); + $entry = ldap_first_entry($ldapconnection, $ldap_result); + do { + $value = ldap_get_values_len($ldapconnection, $entry,$CFG->ldap_user_attribute); + $value = $value[0]; + $count++; + array_push($fresult, $value); + if(count($fresult) >= $bulk_insert_records){ + auth_ldap_bulk_insert($fresult); + //print var_dump($fresult); + $fresult=array(); + } } + while ($entry = ldap_next_entry($ldapconnection, $entry)); - $userids = array_keys($ldapusers); - // bulk insert -- superfast with $bulk_insert_records - while(count($userids)){ - $sql = 'INSERT INTO '.$CFG->prefix.'extuser (idnumber) VALUES '; - $values = array_splice($userids, -($bulk_insert_records) ); - // make those values safe - array_map('addslashes', $values); - // join and quote the whole lot - $sql = $sql . '(\'' . join('\'),(\'', $values) . '\')'; - execute_sql($sql); + // insert any remaining users and release mem + if(count($fresult)){ + auth_ldap_bulk_insert($fresult); + $fresult=array(); } - - /// REMOVE execute_sql('delete from mdl_user where idnumber like \'%s\''); - - // find users in DB that aren't in ldap -- to be removed! - $sql = 'SELECT u.* - FROM ' . $CFG->prefix .'user u LEFT JOIN ' . $CFG->prefix .'extuser e - ON u.idnumber = e.idnumber - WHERE u.auth=\'ldap\' AND u.deleted=\'0\' AND e.idnumber IS NULL'; - $remove_users = get_records_sql($sql); + commit_sql(); + } + // free mem + $ldap_results = 0; + + /// preserve our user database + /// if the temp table is empty, it probably means that something went wrong, exit + /// so as to avoid mass deletion of users; which is hard to undo + $count = get_record_sql('SELECT COUNT(idnumber) AS count, 1 FROM ' . $CFG->prefix .'extuser'); + $count = $count->{'count'}; + if($count < 1){ + print "Did not get any users from LDAP -- error? -- exiting\n"; + exit; + } + + //// + //// User removal + //// + // find users in DB that aren't in ldap -- to be removed! + // this is still not as scalable + $sql = 'SELECT u.id, u.username + FROM ' . $CFG->prefix .'user u LEFT JOIN ' . $CFG->prefix .'extuser e + ON u.idnumber = e.idnumber + WHERE u.auth=\'ldap\' AND u.deleted=\'0\' AND e.idnumber IS NULL'; + //print($sql); + $remove_users = get_records_sql($sql); + + if (!empty($remove_users)){ print "User entries to remove: ". count($remove_users) . "\n"; + + begin_sql(); + foreach ($remove_users as $user) { + //following is copy pasted from admin/user.php + //maybe this should moved to function in lib/datalib.php + unset($updateuser); + $updateuser->id = $user->id; + $updateuser->deleted = "1"; + //$updateuser->username = "$user->username".time(); // Remember it just in case + //$updateuser->email = ""; // Clear this field to free it up + $updateuser->timemodified = time(); + if (update_record("user", $updateuser)) { + unenrol_student($user->id); // From all courses + remove_teacher($user->id); // From all courses + remove_admin($user->id); + notify(get_string("deletedactivity", "", fullname($user, true)) ); + } else { + notify(get_string("deletednot", "", fullname($user, true))); + } + //copy pasted part ends + } + commit_sql(); + } + $remove_users = 0; // free mem! + + //// + //// User Updates + //// (time-consuming, optional) + //// + if ($do_updates) { + // narrow down what fields we need to update + $all_keys = array_keys(get_object_vars($CFG)); + $updatekeys = array(); + foreach ($all_keys as $key) { + if (preg_match('/^auth_user_(.+)_updatelocal$/',$key, $match)) { + if ($CFG->{$match[0]}) { // if it has a true value + array_push($updatekeys, $match[1]); // the actual key name + } + } + } + // print_r($all_keys); print_r($updatekeys); + unset($all_keys); unset($key); - // find users missing in DB that are in LDAP - // note that get_records_sql wants at least 2 fields returned, - // and gives me a nifty object I don't want. - $sql = 'SELECT e.idnumber,1 - FROM ' . $CFG->prefix .'extuser e LEFT JOIN ' . $CFG->prefix .'user u - ON e.idnumber = u.idnumber - WHERE u.id IS NULL'; - $add_users = array_keys(get_records_sql($sql)) || array(); // get rid of the fat - print "User entries to add: ". count($add_users). "\n"; + } + if ( $do_updates && !(empty($updatekeys)) ) { // run updates only if relevant + $users = get_records_sql('SELECT u.username, u.id FROM ' . $CFG->prefix . 'user AS u WHERE u.deleted=0 and u.auth=\'ldap\'' ); + if (!empty($users)) { + print "User entries to update: ". count($users). "\n"; + + + + begin_sql(); + $xcount=0; $maxxcount=100; + foreach ($users as $user) { + echo "updating user $user->username \n"; + auth_ldap_update_user_record($user->username, $updatekeys); + // update course creators + if ( !empty($CFG->ldap_creators) && !empty($CFG->ldap_memberattribute) ) { + if (auth_iscreator($user->username)) { + if (! record_exists("user_coursecreators", "userid", $user->id)) { + $creator = insert_record("user_coursecreators",$user->id); + if (! $creator) { + error("Cannot add user to course creators."); + } + } + } else { + if ( record_exists("user_coursecreators", "userid", $user->id)) { + $creator = delete_records("user_coursecreators", "userid", $user->id); + if (! $creator) { + error("Cannot remove user from course creators."); + } + } + } + } + if ($xcount++ > $maxxcount) { + commit_sql(); + begin_sql(); + $xcount=0; + } + } + commit_sql(); + $users = 0; // free mem + } + } // end do updates - foreach ($ldapusers as $user) { + //// + //// User Additions + //// + // find users missing in DB that are in LDAP + // note that get_records_sql wants at least 2 fields returned, + // and gives me a nifty object I don't want. + $sql = 'SELECT e.idnumber,1 + FROM ' . $CFG->prefix .'extuser e LEFT JOIN ' . $CFG->prefix .'user u + ON e.idnumber = u.idnumber + WHERE u.id IS NULL OR (u.id IS NOT NULL AND u.deleted=1)'; + $add_users = get_records_sql($sql); // get rid of the fat - $usedidnumbers[] = $user->idnumber; //we will need all used idnumbers later - //update modified time - $user->modified = time(); - //All users are confirmed + if(!empty($add_users)){ + print "User entries to add: ". count($add_users). "\n"; + begin_sql(); + foreach($add_users as $user){ + $user = auth_get_userinfo_asobj($user->idnumber); + //print $user->username . "\n"; + + // prep a few params + $user->modified = time(); $user->confirmed = 1; - // if user does not exist create it - if ( is_array($add_users) && in_array($user->idnumber, $add_users) ) { - if (insert_record ('user',$user)) { - echo "inserted user $user->username with idnumber $user->idnumber \n"; - } else { - echo "error inserting user $user->username with idnumber $user->idnumber \n"; - } - update_user_record($user->username); - continue ; + $user->auth = 'ldap'; + + // insert it + $old_debug=$CFG->debug; + $CFG->debug=10; + + // maybe the user has been deleted before + if ($old_user = get_record('user', 'idnumber', $user->idnumber, 'deleted', 1)) { + $user->id = $old_user->id; + set_field('user', 'deleted', 0, 'idnumber', $user->idnumber); + echo "Revived user $user->username with idnumber $user->idnumber id $user->id\n"; + } elseif ($id=insert_record ('user',$user)) { // it is truly a new user + echo "inserted user $user->username with idnumber $user->idnumber id $id\n"; + $user->id = $id; } else { - //update username - set_field('user', 'username', $user->username , 'auth', 'ldap', 'idnumber', $user->idnumber); - //no id-information in ldap so get now - update_user_record($user->username); - $userid = get_field('user', 'id', 'auth', 'ldap', 'idnumber', $user->idnumber); - + echo "error inserting user $user->username with idnumber $user->idnumber \n"; + } + $CFG->debug=$old_debug; + $userobj = auth_ldap_update_user_record($user->username); + if(isset($CFG->{'auth_ldap_forcechangepassword'}) && $CFG->{'auth_ldap_forcechangepassword'}){ + set_user_preference('auth_forcepasswordchange', 1, $userobj); + } + + // update course creators + if ( !empty($CFG->ldap_creators) && !empty($CFG->ldap_memberattribute) ) { if (auth_iscreator($user->username)) { - if (! record_exists("user_coursecreators", "userid", $userid)) { - $cdata['userid']=$userid; - $creator = insert_record("user_coursecreators",$cdata); + if (! record_exists("user_coursecreators", "userid", $user->id)) { + $creator = insert_record("user_coursecreators",$user->id); if (! $creator) { error("Cannot add user to course creators."); - } } + } } else { - if ( record_exists("user_coursecreators", "userid", $userid)) { - $creator = delete_records("user_coursecreators", "userid", $userid); - if (! $creator) { - error("Cannot remove user from course creators."); - } - } + if ( record_exists("user_coursecreators", "userid", $user->id)) { + $creator = delete_records("user_coursecreators", "userid", $$user->id); + if (! $creator) { + error("Cannot remove user from course creators."); + } + } } } - } - - - $result=(is_array($remove_users) ? $remove_users : array()); + } + commit_sql(); + $add_users = 0; // free mem + } + return true; +} + +function auth_ldap_update_user_record($username, $updatekeys=false) { +/// will update a local user record from an external source. +/// is a lighter version of the one in moodlelib -- won't do +/// expensive ops such as enrolment +/// +/// If you don't pass $updatekeys, there is a performance hit and +/// values removed from LDAP won't be removed from moodle. + + global $CFG; + + //just in case check text case + $username = trim(moodle_strtolower($username)); - if (!empty($result)){ - foreach ($result as $user) { - //following is copy pasted from admin/user.php - //maybe this should moved to function in lib/datalib.php - unset($updateuser); - $updateuser->id = $user->id; - $updateuser->deleted = "1"; - $updateuser->username = "$user->email.".time(); // Remember it just in case - $updateuser->email = ""; // Clear this field to free it up - $updateuser->timemodified = time(); - if (update_record("user", $updateuser)) { - unenrol_student($updateuser->id); // From all courses - remove_teacher($updateuser->id); // From all courses - remove_admin($iupdateuser->id); - notify(get_string("deletedactivity", "", fullname($updateuser, true)) ); + // get the current user record + $user = get_record('user', 'username', $username); + if (empty($user)) { // trouble + error_log("Cannot update non-existent user: $username"); + die; + } + + if (function_exists('auth_get_userinfo')) { + if ($newinfo = auth_get_userinfo($username)) { + $newinfo = truncate_userinfo($newinfo); + + if (empty($updatekeys)) { // all keys? this does not support removing values + $updatekeys = array_keys($newinfo); + } + + foreach ($updatekeys as $key){ + unset($value); + if (isset($newinfo[$key])) { + $value = $newinfo[$key]; + $value = addslashes(stripslashes($value)); // Just in case } else { - notify(get_string("deletednot", "", fullname($updateuser, true))); + $value = ''; } - //copy pasted part ends - } + if(isset($CFG->{'auth_user_' . $key. '_updatelocal'}) + && $CFG->{'auth_user_' . $key. '_updatelocal'}){ + if ($user->{$key} != $value) { // only update if it's changed + set_field('user', $key, $value, 'username', $username); + } + } + } } - } //UNSAFEOPTIMIZATIONS ENDS + } + return get_record_select("user", "username = '$username' AND deleted <> '1'"); +} + +function auth_ldap_bulk_insert($users){ +// bulk insert in SQL's temp table +// $users is an array of usernames + global $CFG; + + // bulk insert -- superfast with $bulk_insert_records + $sql = 'INSERT INTO '.$CFG->prefix.'extuser (idnumber) VALUES '; + // make those values safe + array_map('addslashes', $users); + // join and quote the whole lot + $sql = $sql . '(\'' . join('\'),(\'', $users) . '\')'; + print "+ " . count($users) . " users\n"; + execute_sql($sql, false); + } + /* * auth_user_activate activates user in external db. * @@ -595,11 +684,11 @@ function auth_user_activate ($username) { global $CFG; $ldapconnection = auth_ldap_connect(); - + $userdn = auth_ldap_find_userdn($ldapconnection, $username); switch ($CFG->ldap_user_type) { case 'edir': - $newinfo['loginDisabled']="FALSE"; + $newinfo['loginDisabled']="FALSE"; default; error ('auth: ldap auth_user_activate() does not support selected usertype (..yet)'); } @@ -624,7 +713,7 @@ function auth_user_disable ($username) { $userdn = auth_ldap_find_userdn($ldapconnection, $username); switch ($CFG->ldap_user_type) { case 'edir': - $newinfo['loginDisabled']="TRUE"; + $newinfo['loginDisabled']="TRUE"; default: error ('auth: ldap auth_user_disable() does not support selected usertype (..yet)'); } @@ -680,9 +769,14 @@ function auth_user_update($olduser, $newuser) { $search_attribs = array(); $attrmap = auth_ldap_attributes(); - foreach ($attrmap as $key=>$value) { - if (!in_array($value, $search_attribs)) { - array_push($search_attribs, $value); + foreach ($attrmap as $key=>$values) { + if (!is_array($values)) { + $values = array($values); + } + foreach ($values as $value) { + if (!in_array($value, $search_attribs)) { + array_push($search_attribs, $value); + } } } @@ -694,17 +788,59 @@ function auth_user_update($olduser, $newuser) { $user_entry = auth_ldap_get_entries($ldapconnection, $user_info_result); //error_log(var_export($user_entry) . 'fpp' ); - - foreach ($attrmap as $key=>$ldapkey){ + + foreach ($attrmap as $key=>$ldapkeys){ if (isset($CFG->{'auth_user_'. $key.'_updateremote'}) && $CFG->{'auth_user_'. $key.'_updateremote'}){ - // skip update if the values already match - if( !($newuser->$key === $user_entry[0][strtolower($ldapkey)][0]) ){ - ldap_modify($ldapconnection, $user_dn, array($ldapkey => utf8_encode($newuser->$key))); - } else { + + // for ldap values that could be in more than one + // ldap key, we will do our best to match + // where they came from + $ambiguous = true; + $changed = false; + if (!is_array($ldapkeys)) { + $ldapkeys = $array($ldapkeys); + } + if (count($ldapkeys) < 2) { + $ambiguous = false; + } + + foreach ($ldapkeys as $ldapkey) { + if (!$ambiguous) { + // skip update if the values already match + if( !($newuser->$key === $user_entry[0][strtolower($ldapkey)][0]) ){ + ldap_modify($ldapconnection, $user_dn, array($ldapkey => $newuser->$key)); + } else { error_log("Skip updating field $key for entry $user_dn: it seems to be already same on LDAP. " . + " old moodle value: '" . $olduser->$key . + "' new value '" . $newuser->$key . + "' current value in ldap entry " . $user_entry[0][strtolower($ldapkey)][0]); + } + } else { // ambiguous + // check the old values match + //error_log("keys $key $ldapkey"); + //error_log("olduser " . $olduser->$key); + //error_log("ldapuser " . $user_entry[0][strtolower($ldapkey)][0]); + if ( !empty($olduser->$key) + && !empty($user_entry[0][strtolower($ldapkey)][0]) + && $olduser->$key === $user_entry[0][strtolower($ldapkey)][0] ) { + // we found which value to update! + error_log("Matched: ". $olduser->$key . " === " . $user_entry[0][strtolower($ldapkey)][0]); + if(ldap_modify($ldapconnection, $user_dn, array($ldapkey => $newuser->$key))){ + $changed=true; + last; + } else { + error ('Error updating LDAP record. Error code: ' + . ldap_errno($ldapconnection) . '; Error string : ' + . ldap_err2str(ldap_errno($ldapconnection))); + } + } + } + } + + if ($ambiguous AND !$changed) { + error_log("Failed to update LDAP with ambiguous field $key". " old moodle value: '" . $olduser->$key . - "' new value '" . $newuser->$key . - "' current value in ldap entry " . $user_entry[0][strtolower($ldapkey)][0]); + "' new value '" . $newuser->$key ); } } } @@ -717,7 +853,7 @@ function auth_user_update($olduser, $newuser) { } @ldap_close($ldapconnection); - + return true; } @@ -734,7 +870,7 @@ function auth_user_update($olduser, $newuser) { * @return boolean result * */ -function auth_user_update_password($username, $newpassword ) { +function auth_user_update_password($username, $newpassword) { /// called when the user password is updated -- it assumes it is called by an admin /// or that you've otherwise checked the user's credentials /// IMPORTANT: $newpassword must be cleartext, not crypted/md5'ed @@ -754,7 +890,7 @@ function auth_user_update_password($username, $newpassword ) { switch ($CFG->ldap_user_type) { case 'edir': //Change password - $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $newpassword)); + $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $newpassword)); if(!$result){ error_log('LDAP Error in auth_user_update_password(). Error code: ' . ldap_errno($ldapconnection) . '; Error string : ' @@ -778,7 +914,7 @@ function auth_user_update_password($username, $newpassword ) { if (!empty($info[0]['loginGraceLimit'][0])) { $newattrs['loginGraceRemaining']= $info[0]['loginGraceLimit'][0]; } - + //Store attribute changes to ldap $result = ldap_modify($ldapconnection, $user_dn, $newattrs); if(!$result){ @@ -797,12 +933,12 @@ function auth_user_update_password($username, $newpassword ) { $usedconnection = &$ldapconnection; // send ldap the password in cleartext, it will md5 it itself $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $newpassword)); - if(!$result){ - error_log('LDAP Error in auth_user_update_password(). Error code: ' - . ldap_errno($ldapconnection) . '; Error string : ' - . ldap_err2str(ldap_errno($ldapconnection))); - } - + if(!$result){ + error_log('LDAP Error in auth_user_update_password(). Error code: ' + . ldap_errno($ldapconnection) . '; Error string : ' + . ldap_err2str(ldap_errno($ldapconnection))); + } + } @@ -914,15 +1050,15 @@ function auth_ldap_getbinaryfields () { } else { return $binaryfields['default']; } -} - + } + function auth_ldap_isbinary ($field) { if (!isset($field)) { return null ; } return array_search($field, auth_ldap_getbinaryfields()); } - + /** * set $CFG-values for ldap_module * @@ -955,8 +1091,8 @@ function auth_ldap_init () { //hack prefix to objectclass if ('objectClass=' != substr($CFG->ldap_objectclass, 0, 12)) { $CFG->ldap_objectclass = 'objectClass='.$CFG->ldap_objectclass; - } - + } + //all chages go in $CFG , no need to return value } @@ -990,7 +1126,7 @@ function auth_ldap_expirationtime2unix ($time) { break; default: error('CFG->ldap_user_type not defined or function auth_ldap_expirationtime2unix does not support selected type!'); - } +} return $result; } @@ -1035,8 +1171,8 @@ function auth_ldap_isgroupmember ($username='', $groupdns='') { if (empty($username) OR empty($groupdns)) { return $result; - } - + } + if ($CFG->ldap_memberattribute_isdn) { $username=auth_ldap_find_userdn($ldapconnection, $username); } @@ -1056,9 +1192,9 @@ function auth_ldap_isgroupmember ($username='', $groupdns='') { $result = true; break; } - } } - +} + return $result; } @@ -1077,7 +1213,7 @@ function auth_ldap_connect($binddn='',$bindpwd=''){ global $CFG; auth_ldap_init(); - + //Select bind password, With empty values use //ldap_bind_* variables or anonymous bind if ldap_bind_* are empty if ($binddn == '' AND $bindpwd == '') { @@ -1102,20 +1238,20 @@ function auth_ldap_connect($binddn='',$bindpwd=''){ if (!empty($binddn)){ //bind with search-user $bindresult=@ldap_bind($connresult, $binddn,$bindpwd); - } else { - //bind anonymously + } else { + //bind anonymously $bindresult=@ldap_bind($connresult); - } + } if (isset($CFG->ldap_opt_deref)) { ldap_set_option($connresult, LDAP_OPT_DEREF, $CFG->ldap_opt_deref); - } - + } + if ($bindresult) { return $connresult; } - } - +} + //If any of servers are alive we have already returned connection error("LDAP-module cannot connect any LDAP servers : $CFG->ldap_host_url"); return false; @@ -1189,6 +1325,9 @@ function auth_ldap_attributes (){ foreach ($fields as $field) { if (!empty($config["auth_user_$field"])) { $moodleattributes[$field] = $config["auth_user_$field"]; + if (preg_match('/,/',$moodleattributes[$field])) { + $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? + } } } $moodleattributes['username']=$config["ldap_user_attribute"]; @@ -1262,7 +1401,7 @@ function auth_ldap_get_entries($conn, $searchresult){ for($j=0; $j<$attributes['count']; $j++) { $values = ldap_get_values_len($conn, $entry,$attributes[$j]); if (is_array($values)) { - $fresult[$i][$attributes[$j]] = $values; + $fresult[$i][$attributes[$j]] = $values; } else { $fresult[$i][$attributes[$j]] = array($values); } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index fc0218d83e..a7e614433c 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -1289,6 +1289,7 @@ function create_user_record($username, $password, $auth='') { if (function_exists('auth_get_userinfo')) { if ($newinfo = auth_get_userinfo($username)) { + $newinfo = truncate_userinfo($newinfo); foreach ($newinfo as $key => $value){ $newuser->$key = addslashes(stripslashes($value)); // Just in case } @@ -1344,6 +1345,38 @@ function update_user_record($username) { return get_user_info_from_db('username', $username); } +function truncate_userinfo($info) { +/// will truncate userinfo as it comes from auth_get_userinfo (from external auth) +/// which may have large fields + + // define the limits + $limit = array( + 'username' => 100, + 'idnumber' => 12, + 'firstname' => 20, + 'lastname' => 20, + 'email' => 100, + 'icq' => 15, + 'phone1' => 20, + 'phone2' => 20, + 'institution' => 40, + 'department' => 30, + 'address' => 70, + 'city' => 20, + 'country' => 2, + 'url' => 255, + ); + + // apply where needed + foreach (array_keys($info) as $key) { + if (!empty($limit[$key])) { + $info[$key] = substr($info[$key],0, $limit[$key]); + } + } + + return $info; +} + /** * Retrieve the guest user object * diff --git a/user/edit.php b/user/edit.php index 4cd9388d9e..ae10e91fdd 100644 --- a/user/edit.php +++ b/user/edit.php @@ -98,11 +98,12 @@ } } + $usernew->auth = $user->auth; $user = $usernew; } else { $timenow = time(); - + if (!$usernew->picture = save_profile_image($user->id,$um,'users')) { if (!empty($usernew->deletepicture)) { set_field('user', 'picture', 0, 'id', $user->id); /// Delete picture @@ -145,9 +146,11 @@ } if (update_record("user", $usernew)) { - if (function_exists("auth_user_update")){ - auth_user_update($user, $usernew); - } + if (function_exists("auth_user_update")){ + // pass a true $userold here + auth_user_update($userold, $usernew); + }; + add_to_log($course->id, "user", "update", "view.php?id=$user->id&course=$course->id", ""); if ($user->id == $USER->id) {