PageRenderTime 77ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 1ms

/auth/ldap/auth.php

https://bitbucket.org/ceu/moodle_demo
PHP | 2301 lines | 1460 code | 260 blank | 581 comment | 298 complexity | e430a2f4f283fbf57ba16176ca6fa223 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.0, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * @author Martin Dougiamas
  4. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  5. * @package moodle multiauth
  6. *
  7. * Authentication Plugin: LDAP Authentication
  8. *
  9. * Authentication using LDAP (Lightweight Directory Access Protocol).
  10. *
  11. * 2006-08-28 File created.
  12. */
  13. if (!defined('MOODLE_INTERNAL')) {
  14. die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
  15. }
  16. // See http://support.microsoft.com/kb/305144 to interprete these values.
  17. if (!defined('AUTH_AD_ACCOUNTDISABLE')) {
  18. define('AUTH_AD_ACCOUNTDISABLE', 0x0002);
  19. }
  20. if (!defined('AUTH_AD_NORMAL_ACCOUNT')) {
  21. define('AUTH_AD_NORMAL_ACCOUNT', 0x0200);
  22. }
  23. if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs...
  24. define('AUTH_NTLMTIMEOUT', 10);
  25. }
  26. require_once($CFG->libdir.'/authlib.php');
  27. /**
  28. * LDAP authentication plugin.
  29. */
  30. class auth_plugin_ldap extends auth_plugin_base {
  31. /**
  32. * Constructor with initialisation.
  33. */
  34. function auth_plugin_ldap() {
  35. $this->authtype = 'ldap';
  36. $this->config = get_config('auth/ldap');
  37. if (empty($this->config->ldapencoding)) {
  38. $this->config->ldapencoding = 'utf-8';
  39. }
  40. if (empty($this->config->user_type)) {
  41. $this->config->user_type = 'default';
  42. }
  43. $default = $this->ldap_getdefaults();
  44. //use defaults if values not given
  45. foreach ($default as $key => $value) {
  46. // watch out - 0, false are correct values too
  47. if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
  48. $this->config->{$key} = $value[$this->config->user_type];
  49. }
  50. }
  51. // Hack prefix to objectclass
  52. if (empty($this->config->objectclass)) {
  53. // Can't send empty filter
  54. $this->config->objectclass='(objectClass=*)';
  55. } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
  56. // Value is 'objectClass=some-string-here', so just add ()
  57. // around the value (filter _must_ have them).
  58. $this->config->objectclass = '('.$this->config->objectclass.')';
  59. } else if (stripos($this->config->objectclass, '(') !== 0) {
  60. // Value is 'some-string-not-starting-with-left-parentheses',
  61. // which is assumed to be the objectClass matching value.
  62. // So build a valid filter with it.
  63. $this->config->objectclass = '(objectClass='.$this->config->objectclass.')';
  64. } else {
  65. // There is an additional possible value
  66. // '(some-string-here)', that can be used to specify any
  67. // valid filter string, to select subsets of users based
  68. // on any criteria. For example, we could select the users
  69. // whose objectClass is 'user' and have the
  70. // 'enabledMoodleUser' attribute, with something like:
  71. //
  72. // (&(objectClass=user)(enabledMoodleUser=1))
  73. //
  74. // This is only used in the functions that deal with the
  75. // whole potential set of users (currently sync_users()
  76. // and get_user_list() only).
  77. //
  78. // In this particular case we don't need to do anything,
  79. // so leave $this->config->objectclass as is.
  80. }
  81. }
  82. /**
  83. * Returns true if the username and password work and false if they are
  84. * wrong or don't exist.
  85. *
  86. * @param string $username The username (with system magic quotes)
  87. * @param string $password The password (with system magic quotes)
  88. *
  89. * @return bool Authentication success or failure.
  90. */
  91. function user_login($username, $password) {
  92. if (! function_exists('ldap_bind')) {
  93. print_error('auth_ldapnotinstalled','auth');
  94. return false;
  95. }
  96. if (!$username or !$password) { // Don't allow blank usernames or passwords
  97. return false;
  98. }
  99. $textlib = textlib_get_instance();
  100. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  101. $extpassword = $textlib->convert(stripslashes($password), 'utf-8', $this->config->ldapencoding);
  102. //
  103. // Before we connect to LDAP, check if this is an AD SSO login
  104. // if we succeed in this block, we'll return success early.
  105. //
  106. $key = sesskey();
  107. if (!empty($this->config->ntlmsso_enabled) && $key === $password) {
  108. $cf = get_cache_flags('auth/ldap/ntlmsess');
  109. // We only get the cache flag if we retrieve it before
  110. // it expires (AUTH_NTLMTIMEOUT seconds).
  111. if (!isset($cf[$key]) || $cf[$key] === '') {
  112. return false;
  113. }
  114. $sessusername = $cf[$key];
  115. if ($username === $sessusername) {
  116. unset($sessusername);
  117. unset($cf);
  118. // Check that the user is inside one of the configured LDAP contexts
  119. $validuser = false;
  120. $ldapconnection = $this->ldap_connect();
  121. if ($ldapconnection) {
  122. // if the user is not inside the configured contexts,
  123. // ldap_find_userdn returns false.
  124. if ($this->ldap_find_userdn($ldapconnection, $extusername)) {
  125. $validuser = true;
  126. }
  127. $this->ldap_close();
  128. }
  129. // Shortcut here - SSO confirmed
  130. return $validuser;
  131. }
  132. } // End SSO processing
  133. unset($key);
  134. $ldapconnection = $this->ldap_connect();
  135. if ($ldapconnection) {
  136. $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  137. //if ldap_user_dn is empty, user does not exist
  138. if (!$ldap_user_dn) {
  139. $this->ldap_close();
  140. return false;
  141. }
  142. // Try to bind with current username and password
  143. $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword);
  144. $this->ldap_close();
  145. if ($ldap_login) {
  146. return true;
  147. }
  148. }
  149. else {
  150. $this->ldap_close();
  151. print_error('auth_ldap_noconnect','auth','',$this->config->host_url);
  152. }
  153. return false;
  154. }
  155. /**
  156. * reads userinformation from ldap and return it in array()
  157. *
  158. * Read user information from external database and returns it as array().
  159. * Function should return all information available. If you are saving
  160. * this information to moodle user-table you should honor syncronization flags
  161. *
  162. * @param string $username username (with system magic quotes)
  163. *
  164. * @return mixed array with no magic quotes or false on error
  165. */
  166. function get_userinfo($username) {
  167. $textlib = textlib_get_instance();
  168. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  169. $ldapconnection = $this->ldap_connect();
  170. $attrmap = $this->ldap_attributes();
  171. $result = array();
  172. $search_attribs = array();
  173. foreach ($attrmap as $key=>$values) {
  174. if (!is_array($values)) {
  175. $values = array($values);
  176. }
  177. foreach ($values as $value) {
  178. if (!in_array($value, $search_attribs)) {
  179. array_push($search_attribs, $value);
  180. }
  181. }
  182. }
  183. $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  184. if (!$user_info_result = ldap_read($ldapconnection, $user_dn, $this->config->objectclass, $search_attribs)) {
  185. return false; // error!
  186. }
  187. $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
  188. if (empty($user_entry)) {
  189. return false; // entry not found
  190. }
  191. foreach ($attrmap as $key=>$values) {
  192. if (!is_array($values)) {
  193. $values = array($values);
  194. }
  195. $ldapval = NULL;
  196. foreach ($values as $value) {
  197. if ((moodle_strtolower($value) == 'dn') || (moodle_strtolower($value) == 'distinguishedname')) {
  198. $result[$key] = $user_dn;
  199. }
  200. if (!array_key_exists($value, $user_entry[0])) {
  201. continue; // wrong data mapping!
  202. }
  203. if (is_array($user_entry[0][$value])) {
  204. $newval = $textlib->convert($user_entry[0][$value][0], $this->config->ldapencoding, 'utf-8');
  205. } else {
  206. $newval = $textlib->convert($user_entry[0][$value], $this->config->ldapencoding, 'utf-8');
  207. }
  208. if (!empty($newval)) { // favour ldap entries that are set
  209. $ldapval = $newval;
  210. }
  211. }
  212. if (!is_null($ldapval)) {
  213. $result[$key] = $ldapval;
  214. }
  215. }
  216. $this->ldap_close();
  217. return $result;
  218. }
  219. /**
  220. * reads userinformation from ldap and return it in an object
  221. *
  222. * @param string $username username (with system magic quotes)
  223. * @return mixed object or false on error
  224. */
  225. function get_userinfo_asobj($username) {
  226. $user_array = $this->get_userinfo($username);
  227. if ($user_array == false) {
  228. return false; //error or not found
  229. }
  230. $user_array = truncate_userinfo($user_array);
  231. $user = new object();
  232. foreach ($user_array as $key=>$value) {
  233. $user->{$key} = $value;
  234. }
  235. return $user;
  236. }
  237. /**
  238. * returns all usernames from external database
  239. *
  240. * get_userlist returns all usernames from external database
  241. *
  242. * @return array
  243. */
  244. function get_userlist() {
  245. return $this->ldap_get_userlist("({$this->config->user_attribute}=*)");
  246. }
  247. /**
  248. * checks if user exists on external db
  249. *
  250. * @param string $username (with system magic quotes)
  251. */
  252. function user_exists($username) {
  253. $textlib = textlib_get_instance();
  254. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  255. //returns true if given username exist on ldap
  256. $users = $this->ldap_get_userlist("({$this->config->user_attribute}=".$this->filter_addslashes($extusername).")");
  257. return count($users);
  258. }
  259. /**
  260. * Creates a new user on external database.
  261. * By using information in userobject
  262. * Use user_exists to prevent dublicate usernames
  263. *
  264. * @param mixed $userobject Moodle userobject (with system magic quotes)
  265. * @param mixed $plainpass Plaintext password (with system magic quotes)
  266. */
  267. function user_create($userobject, $plainpass) {
  268. $textlib = textlib_get_instance();
  269. $extusername = $textlib->convert(stripslashes($userobject->username), 'utf-8', $this->config->ldapencoding);
  270. $extpassword = $textlib->convert(stripslashes($plainpass), 'utf-8', $this->config->ldapencoding);
  271. switch ($this->config->passtype) {
  272. case 'md5':
  273. $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
  274. break;
  275. case 'sha1':
  276. $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
  277. break;
  278. case 'plaintext':
  279. default:
  280. break; // plaintext
  281. }
  282. $ldapconnection = $this->ldap_connect();
  283. $attrmap = $this->ldap_attributes();
  284. $newuser = array();
  285. foreach ($attrmap as $key => $values) {
  286. if (!is_array($values)) {
  287. $values = array($values);
  288. }
  289. foreach ($values as $value) {
  290. if (!empty($userobject->$key) ) {
  291. $newuser[$value] = $textlib->convert(stripslashes($userobject->$key), 'utf-8', $this->config->ldapencoding);
  292. }
  293. }
  294. }
  295. //Following sets all mandatory and other forced attribute values
  296. //User should be creted as login disabled untill email confirmation is processed
  297. //Feel free to add your user type and send patches to paca@sci.fi to add them
  298. //Moodle distribution
  299. switch ($this->config->user_type) {
  300. case 'edir':
  301. $newuser['objectClass'] = array("inetOrgPerson","organizationalPerson","person","top");
  302. $newuser['uniqueId'] = $extusername;
  303. $newuser['logindisabled'] = "TRUE";
  304. $newuser['userpassword'] = $extpassword;
  305. $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'="'.$this->ldap_addslashes($userobject->username).','.$this->config->create_context.'"', $newuser);
  306. break;
  307. case 'ad':
  308. // User account creation is a two step process with AD. First you
  309. // create the user object, then you set the password. If you try
  310. // to set the password while creating the user, the operation
  311. // fails.
  312. // Passwords in Active Directory must be encoded as Unicode
  313. // strings (UCS-2 Little Endian format) and surrounded with
  314. // double quotes. See http://support.microsoft.com/?kbid=269190
  315. if (!function_exists('mb_convert_encoding')) {
  316. print_error ('auth_ldap_no_mbstring', 'auth');
  317. }
  318. // First create the user account, and mark it as disabled.
  319. $newuser['objectClass'] = array('top','person','user','organizationalPerson');
  320. $newuser['sAMAccountName'] = $extusername;
  321. $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT |
  322. AUTH_AD_ACCOUNTDISABLE;
  323. $userdn = 'cn=' . $this->ldap_addslashes($extusername) .
  324. ',' . $this->config->create_context;
  325. if (!ldap_add($ldapconnection, $userdn, $newuser)) {
  326. print_error ('auth_ldap_ad_create_req', 'auth');
  327. }
  328. // Now set the password
  329. unset($newuser);
  330. $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"',
  331. "UCS-2LE", "UTF-8");
  332. if(!ldap_modify($ldapconnection, $userdn, $newuser)) {
  333. // Something went wrong: delete the user account and error out
  334. ldap_delete ($ldapconnection, $userdn);
  335. print_error ('auth_ldap_ad_create_req', 'auth');
  336. }
  337. $uadd = true;
  338. break;
  339. default:
  340. print_error('auth_ldap_unsupportedusertype','auth','',$this->config->user_type);
  341. }
  342. $this->ldap_close();
  343. return $uadd;
  344. }
  345. function can_reset_password() {
  346. return !empty($this->config->stdchangepassword);
  347. }
  348. function can_signup() {
  349. return (!empty($this->config->auth_user_create) and !empty($this->config->create_context));
  350. }
  351. /**
  352. * Sign up a new user ready for confirmation.
  353. * Password is passed in plaintext.
  354. *
  355. * @param object $user new user object (with system magic quotes)
  356. * @param boolean $notify print notice with link and terminate
  357. */
  358. function user_signup($user, $notify=true) {
  359. global $CFG;
  360. require_once($CFG->dirroot.'/user/profile/lib.php');
  361. if ($this->user_exists($user->username)) {
  362. print_error('auth_ldap_user_exists', 'auth');
  363. }
  364. $plainslashedpassword = $user->password;
  365. unset($user->password);
  366. if (! $this->user_create($user, $plainslashedpassword)) {
  367. print_error('auth_ldap_create_error', 'auth');
  368. }
  369. if (! ($user->id = insert_record('user', $user)) ) {
  370. print_error('auth_emailnoinsert', 'auth');
  371. }
  372. /// Save any custom profile field information
  373. profile_save_data($user);
  374. $this->update_user_record($user->username);
  375. update_internal_user_password($user, $plainslashedpassword);
  376. $user = get_record('user', 'id', $user->id);
  377. events_trigger('user_created', $user);
  378. if (! send_confirmation_email($user)) {
  379. print_error('auth_emailnoemail', 'auth');
  380. }
  381. if ($notify) {
  382. global $CFG;
  383. $emailconfirm = get_string('emailconfirm');
  384. $navlinks = array();
  385. $navlinks[] = array('name' => $emailconfirm, 'link' => null, 'type' => 'misc');
  386. $navigation = build_navigation($navlinks);
  387. print_header($emailconfirm, $emailconfirm, $navigation);
  388. notice(get_string('emailconfirmsent', '', $user->email), "$CFG->wwwroot/index.php");
  389. } else {
  390. return true;
  391. }
  392. }
  393. /**
  394. * Returns true if plugin allows confirming of new users.
  395. *
  396. * @return bool
  397. */
  398. function can_confirm() {
  399. return $this->can_signup();
  400. }
  401. /**
  402. * Confirm the new user as registered.
  403. *
  404. * @param string $username (with system magic quotes)
  405. * @param string $confirmsecret (with system magic quotes)
  406. */
  407. function user_confirm($username, $confirmsecret) {
  408. $user = get_complete_user_data('username', $username);
  409. if (!empty($user)) {
  410. if ($user->confirmed) {
  411. return AUTH_CONFIRM_ALREADY;
  412. } else if ($user->auth != 'ldap') {
  413. return AUTH_CONFIRM_ERROR;
  414. } else if ($user->secret == stripslashes($confirmsecret)) { // They have provided the secret key to get in
  415. if (!$this->user_activate($username)) {
  416. return AUTH_CONFIRM_FAIL;
  417. }
  418. if (!set_field("user", "confirmed", 1, "id", $user->id)) {
  419. return AUTH_CONFIRM_FAIL;
  420. }
  421. if (!set_field("user", "firstaccess", time(), "id", $user->id)) {
  422. return AUTH_CONFIRM_FAIL;
  423. }
  424. return AUTH_CONFIRM_OK;
  425. }
  426. } else {
  427. return AUTH_CONFIRM_ERROR;
  428. }
  429. }
  430. /**
  431. * return number of days to user password expires
  432. *
  433. * If userpassword does not expire it should return 0. If password is already expired
  434. * it should return negative value.
  435. *
  436. * @param mixed $username username (with system magic quotes)
  437. * @return integer
  438. */
  439. function password_expire($username) {
  440. $result = 0;
  441. $textlib = textlib_get_instance();
  442. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  443. $ldapconnection = $this->ldap_connect();
  444. $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  445. $search_attribs = array($this->config->expireattr);
  446. $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
  447. if ($sr) {
  448. $info = $this->ldap_get_entries($ldapconnection, $sr);
  449. if (!empty ($info) and !empty($info[0][$this->config->expireattr][0])) {
  450. $expiretime = $this->ldap_expirationtime2unix($info[0][$this->config->expireattr][0], $ldapconnection, $user_dn);
  451. if ($expiretime != 0) {
  452. $now = time();
  453. if ($expiretime > $now) {
  454. $result = ceil(($expiretime - $now) / DAYSECS);
  455. }
  456. else {
  457. $result = floor(($expiretime - $now) / DAYSECS);
  458. }
  459. }
  460. }
  461. } else {
  462. error_log("ldap: password_expire did't find expiration time.");
  463. }
  464. //error_log("ldap: password_expire user $user_dn expires in $result days!");
  465. return $result;
  466. }
  467. /**
  468. * syncronizes user fron external db to moodle user table
  469. *
  470. * Sync is now using username attribute.
  471. *
  472. * Syncing users removes or suspends users that dont exists anymore in external db.
  473. * Creates new users and updates coursecreator status of users.
  474. *
  475. * @param int $bulk_insert_records will insert $bulkinsert_records per insert statement
  476. * valid only with $unsafe. increase to a couple thousand for
  477. * blinding fast inserts -- but test it: you may hit mysqld's
  478. * max_allowed_packet limit.
  479. * @param bool $do_updates will do pull in data updates from ldap if relevant
  480. */
  481. function sync_users ($bulk_insert_records = 1000, $do_updates = true) {
  482. global $CFG;
  483. $textlib = textlib_get_instance();
  484. $droptablesql = array(); /// sql commands to drop the table (because session scope could be a problem for
  485. /// some persistent drivers like ODBTP (mssql) or if this function is invoked
  486. /// from within a PHP application using persistent connections
  487. $temptable = $CFG->prefix . 'extuser';
  488. $createtemptablesql = '';
  489. // configure a temp table
  490. print "Configuring temp table\n";
  491. switch (strtolower($CFG->dbfamily)) {
  492. case 'mysql':
  493. $droptablesql[] = 'DROP TEMPORARY TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  494. $createtemptablesql = 'CREATE TEMPORARY TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username)) ENGINE=MyISAM';
  495. break;
  496. case 'postgres':
  497. $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  498. $bulk_insert_records = 1; // no support for multiple sets of values
  499. $createtemptablesql = 'CREATE TEMPORARY TABLE '. $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))';
  500. break;
  501. case 'mssql':
  502. $temptable = '#'. $temptable; /// MSSQL temp tables begin with #
  503. $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  504. $bulk_insert_records = 1; // no support for multiple sets of values
  505. $createtemptablesql = 'CREATE TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))';
  506. break;
  507. case 'oracle':
  508. $droptablesql[] = 'TRUNCATE TABLE ' . $temptable; // oracle requires truncate before being able to drop a temp table
  509. $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  510. $bulk_insert_records = 1; // no support for multiple sets of values
  511. $createtemptablesql = 'CREATE GLOBAL TEMPORARY TABLE '.$temptable.' (username VARCHAR(64), PRIMARY KEY (username)) ON COMMIT PRESERVE ROWS';
  512. break;
  513. }
  514. execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
  515. echo "Creating temp table $temptable\n";
  516. if(! execute_sql($createtemptablesql, false) ){
  517. print "Failed to create temporary users table - aborting\n";
  518. exit;
  519. }
  520. print "Connecting to ldap...\n";
  521. $ldapconnection = $this->ldap_connect();
  522. if (!$ldapconnection) {
  523. $this->ldap_close();
  524. print get_string('auth_ldap_noconnect','auth',$this->config->host_url);
  525. exit;
  526. }
  527. ////
  528. //// get user's list from ldap to sql in a scalable fashion
  529. ////
  530. // prepare some data we'll need
  531. $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
  532. $contexts = explode(";",$this->config->contexts);
  533. if (!empty($this->config->create_context)) {
  534. array_push($contexts, $this->config->create_context);
  535. }
  536. $fresult = array();
  537. foreach ($contexts as $context) {
  538. $context = trim($context);
  539. if (empty($context)) {
  540. continue;
  541. }
  542. begin_sql();
  543. if ($this->config->search_sub) {
  544. //use ldap_search to find first user from subtree
  545. $ldap_result = ldap_search($ldapconnection, $context,
  546. $filter,
  547. array($this->config->user_attribute));
  548. } else {
  549. //search only in this context
  550. $ldap_result = ldap_list($ldapconnection, $context,
  551. $filter,
  552. array($this->config->user_attribute));
  553. }
  554. if ($entry = ldap_first_entry($ldapconnection, $ldap_result)) {
  555. do {
  556. $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute);
  557. $value = $textlib->convert($value[0], $this->config->ldapencoding, 'utf-8');
  558. // usernames are __always__ lowercase.
  559. array_push($fresult, moodle_strtolower($value));
  560. if (count($fresult) >= $bulk_insert_records) {
  561. $this->ldap_bulk_insert($fresult, $temptable);
  562. $fresult = array();
  563. }
  564. } while ($entry = ldap_next_entry($ldapconnection, $entry));
  565. }
  566. unset($ldap_result); // free mem
  567. // insert any remaining users and release mem
  568. if (count($fresult)) {
  569. $this->ldap_bulk_insert($fresult, $temptable);
  570. $fresult = array();
  571. }
  572. commit_sql();
  573. }
  574. /// preserve our user database
  575. /// if the temp table is empty, it probably means that something went wrong, exit
  576. /// so as to avoid mass deletion of users; which is hard to undo
  577. $count = get_record_sql('SELECT COUNT(username) AS count, 1 FROM ' . $temptable);
  578. $count = $count->{'count'};
  579. if ($count < 1) {
  580. print "Did not get any users from LDAP -- error? -- exiting\n";
  581. exit;
  582. } else {
  583. print "Got $count records from LDAP\n\n";
  584. }
  585. /// User removal
  586. // find users in DB that aren't in ldap -- to be removed!
  587. // this is still not as scalable (but how often do we mass delete?)
  588. if (!empty($this->config->removeuser)) {
  589. $sql = "SELECT u.id, u.username, u.email, u.auth
  590. FROM {$CFG->prefix}user u
  591. LEFT JOIN $temptable e ON u.username = e.username
  592. WHERE u.auth='ldap'
  593. AND u.deleted=0
  594. AND e.username IS NULL";
  595. $remove_users = get_records_sql($sql);
  596. if (!empty($remove_users)) {
  597. print "User entries to remove: ". count($remove_users) . "\n";
  598. foreach ($remove_users as $user) {
  599. if ($this->config->removeuser == 2) {
  600. if (delete_user($user)) {
  601. echo "\t"; print_string('auth_dbdeleteuser', 'auth', array($user->username, $user->id)); echo "\n";
  602. } else {
  603. echo "\t"; print_string('auth_dbdeleteusererror', 'auth', $user->username); echo "\n";
  604. }
  605. } else if ($this->config->removeuser == 1) {
  606. $updateuser = new object();
  607. $updateuser->id = $user->id;
  608. $updateuser->auth = 'nologin';
  609. if (update_record('user', $updateuser)) {
  610. echo "\t"; print_string('auth_dbsuspenduser', 'auth', array($user->username, $user->id)); echo "\n";
  611. } else {
  612. echo "\t"; print_string('auth_dbsuspendusererror', 'auth', $user->username); echo "\n";
  613. }
  614. }
  615. }
  616. } else {
  617. print "No user entries to be removed\n";
  618. }
  619. unset($remove_users); // free mem!
  620. }
  621. /// Revive suspended users
  622. if (!empty($this->config->removeuser) and $this->config->removeuser == 1) {
  623. $sql = "SELECT u.id, u.username
  624. FROM $temptable e, {$CFG->prefix}user u
  625. WHERE e.username=u.username
  626. AND u.auth='nologin'";
  627. $revive_users = get_records_sql($sql);
  628. if (!empty($revive_users)) {
  629. print "User entries to be revived: ". count($revive_users) . "\n";
  630. begin_sql();
  631. foreach ($revive_users as $user) {
  632. $updateuser = new object();
  633. $updateuser->id = $user->id;
  634. $updateuser->auth = 'ldap';
  635. if (update_record('user', $updateuser)) {
  636. echo "\t"; print_string('auth_dbreviveduser', 'auth', array($user->username, $user->id)); echo "\n";
  637. } else {
  638. echo "\t"; print_string('auth_dbrevivedusererror', 'auth', $user->username); echo "\n";
  639. }
  640. }
  641. commit_sql();
  642. } else {
  643. print "No user entries to be revived\n";
  644. }
  645. unset($revive_users);
  646. }
  647. /// User Updates - time-consuming (optional)
  648. if ($do_updates) {
  649. // narrow down what fields we need to update
  650. $all_keys = array_keys(get_object_vars($this->config));
  651. $updatekeys = array();
  652. foreach ($all_keys as $key) {
  653. if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) {
  654. // if we have a field to update it from
  655. // and it must be updated 'onlogin' we
  656. // update it on cron
  657. if ( !empty($this->config->{'field_map_'.$match[1]})
  658. and $this->config->{$match[0]} === 'onlogin') {
  659. array_push($updatekeys, $match[1]); // the actual key name
  660. }
  661. }
  662. }
  663. // print_r($all_keys); print_r($updatekeys);
  664. unset($all_keys); unset($key);
  665. } else {
  666. print "No updates to be done\n";
  667. }
  668. if ( $do_updates and !empty($updatekeys) ) { // run updates only if relevant
  669. $users = get_records_sql("SELECT u.username, u.id
  670. FROM {$CFG->prefix}user u
  671. WHERE u.deleted=0 AND u.auth='ldap'");
  672. if (!empty($users)) {
  673. print "User entries to update: ". count($users). "\n";
  674. $sitecontext = get_context_instance(CONTEXT_SYSTEM);
  675. if (!empty($this->config->creators) and !empty($this->config->memberattribute)
  676. and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
  677. $creatorrole = array_shift($roles); // We can only use one, let's use the first one
  678. } else {
  679. $creatorrole = false;
  680. }
  681. begin_sql();
  682. $xcount = 0;
  683. $maxxcount = 100;
  684. foreach ($users as $user) {
  685. echo "\t"; print_string('auth_dbupdatinguser', 'auth', array($user->username, $user->id));
  686. if (!$this->update_user_record(addslashes($user->username), $updatekeys)) {
  687. echo " - ".get_string('skipped');
  688. }
  689. echo "\n";
  690. $xcount++;
  691. // update course creators if needed
  692. if ($creatorrole !== false) {
  693. if ($this->iscreator($user->username)) {
  694. role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
  695. } else {
  696. role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'ldap');
  697. }
  698. }
  699. if ($xcount++ > $maxxcount) {
  700. commit_sql();
  701. begin_sql();
  702. $xcount = 0;
  703. }
  704. }
  705. commit_sql();
  706. unset($users); // free mem
  707. }
  708. } else { // end do updates
  709. print "No updates to be done\n";
  710. }
  711. /// User Additions
  712. // find users missing in DB that are in LDAP
  713. // note that get_records_sql wants at least 2 fields returned,
  714. // and gives me a nifty object I don't want.
  715. // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin
  716. $sql = "SELECT e.username, e.username
  717. FROM $temptable e LEFT JOIN {$CFG->prefix}user u ON e.username = u.username
  718. WHERE u.id IS NULL";
  719. $add_users = get_records_sql($sql); // get rid of the fat
  720. if (!empty($add_users)) {
  721. print "User entries to add: ". count($add_users). "\n";
  722. $sitecontext = get_context_instance(CONTEXT_SYSTEM);
  723. if (!empty($this->config->creators) and !empty($this->config->memberattribute)
  724. and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
  725. $creatorrole = array_shift($roles); // We can only use one, let's use the first one
  726. } else {
  727. $creatorrole = false;
  728. }
  729. begin_sql();
  730. foreach ($add_users as $user) {
  731. $user = $this->get_userinfo_asobj(addslashes($user->username));
  732. // prep a few params
  733. $user->modified = time();
  734. $user->confirmed = 1;
  735. $user->auth = 'ldap';
  736. $user->mnethostid = $CFG->mnet_localhost_id;
  737. // get_userinfo_asobj() might have replaced $user->username with the value
  738. // from the LDAP server (which can be mixed-case). Make sure it's lowercase
  739. $user->username = trim(moodle_strtolower($user->username));
  740. if (empty($user->lang)) {
  741. $user->lang = $CFG->lang;
  742. }
  743. $user = addslashes_recursive($user);
  744. if ($id = insert_record('user',$user)) {
  745. echo "\t"; print_string('auth_dbinsertuser', 'auth', array(stripslashes($user->username), $id)); echo "\n";
  746. $userobj = $this->update_user_record($user->username);
  747. if (!empty($this->config->forcechangepassword)) {
  748. set_user_preference('auth_forcepasswordchange', 1, $userobj->id);
  749. }
  750. // add course creators if needed
  751. if ($creatorrole !== false and $this->iscreator(stripslashes($user->username))) {
  752. role_assign($creatorrole->id, $id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
  753. }
  754. } else {
  755. echo "\t"; print_string('auth_dbinsertusererror', 'auth', $user->username); echo "\n";
  756. }
  757. }
  758. commit_sql();
  759. unset($add_users); // free mem
  760. } else {
  761. print "No users to be added\n";
  762. }
  763. $this->ldap_close();
  764. return true;
  765. }
  766. /**
  767. * Update a local user record from an external source.
  768. * This is a lighter version of the one in moodlelib -- won't do
  769. * expensive ops such as enrolment.
  770. *
  771. * If you don't pass $updatekeys, there is a performance hit and
  772. * values removed from LDAP won't be removed from moodle.
  773. *
  774. * @param string $username username (with system magic quotes)
  775. */
  776. function update_user_record($username, $updatekeys = false) {
  777. global $CFG;
  778. //just in case check text case
  779. $username = trim(moodle_strtolower($username));
  780. // get the current user record
  781. $user = get_record('user', 'username', $username, 'mnethostid', $CFG->mnet_localhost_id);
  782. if (empty($user)) { // trouble
  783. error_log("Cannot update non-existent user: ".stripslashes($username));
  784. print_error('auth_dbusernotexist','auth','',$username);
  785. die;
  786. }
  787. // Protect the userid from being overwritten
  788. $userid = $user->id;
  789. if ($newinfo = $this->get_userinfo($username)) {
  790. $newinfo = truncate_userinfo($newinfo);
  791. if (empty($updatekeys)) { // all keys? this does not support removing values
  792. $updatekeys = array_keys($newinfo);
  793. }
  794. foreach ($updatekeys as $key) {
  795. if (isset($newinfo[$key])) {
  796. $value = $newinfo[$key];
  797. } else {
  798. $value = '';
  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);
  803. }
  804. }
  805. }
  806. } else {
  807. return false;
  808. }
  809. return get_record_select('user', "id = $userid AND deleted = 0");
  810. }
  811. /**
  812. * Bulk insert in SQL's temp table
  813. * @param array $users is an array of usernames
  814. */
  815. function ldap_bulk_insert($users, $temptable) {
  816. // bulk insert -- superfast with $bulk_insert_records
  817. $sql = 'INSERT INTO ' . $temptable . ' (username) VALUES ';
  818. // make those values safe
  819. $users = addslashes_recursive($users);
  820. // join and quote the whole lot
  821. $sql = $sql . "('" . implode("'),('", $users) . "')";
  822. print "\t+ " . count($users) . " users\n";
  823. execute_sql($sql, false);
  824. }
  825. /**
  826. * Activates (enables) user in external db so user can login to external db
  827. *
  828. * @param mixed $username username (with system magic quotes)
  829. * @return boolen result
  830. */
  831. function user_activate($username) {
  832. $textlib = textlib_get_instance();
  833. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  834. $ldapconnection = $this->ldap_connect();
  835. $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
  836. switch ($this->config->user_type) {
  837. case 'edir':
  838. $newinfo['loginDisabled']="FALSE";
  839. break;
  840. case 'ad':
  841. // We need to unset the ACCOUNTDISABLE bit in the
  842. // userAccountControl attribute ( see
  843. // http://support.microsoft.com/kb/305144 )
  844. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)',
  845. array('userAccountControl'));
  846. $info = ldap_get_entries($ldapconnection, $sr);
  847. $newinfo['userAccountControl'] = $info[0]['userAccountControl'][0]
  848. & (~AUTH_AD_ACCOUNTDISABLE);
  849. break;
  850. default:
  851. error ('auth: ldap user_activate() does not support selected usertype:"'.$this->config->user_type.'" (..yet)');
  852. }
  853. $result = ldap_modify($ldapconnection, $userdn, $newinfo);
  854. $this->ldap_close();
  855. return $result;
  856. }
  857. /**
  858. * Disables user in external db so user can't login to external db
  859. *
  860. * @param mixed $username username
  861. * @return boolean result
  862. */
  863. /* function user_disable($username) {
  864. $textlib = textlib_get_instance();
  865. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  866. $ldapconnection = $this->ldap_connect();
  867. $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
  868. switch ($this->config->user_type) {
  869. case 'edir':
  870. $newinfo['loginDisabled']="TRUE";
  871. break;
  872. case 'ad':
  873. // We need to set the ACCOUNTDISABLE bit in the
  874. // userAccountControl attribute ( see
  875. // http://support.microsoft.com/kb/305144 )
  876. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)',
  877. array('userAccountControl'));
  878. $info = auth_ldap_get_entries($ldapconnection, $sr);
  879. $newinfo['userAccountControl'] = $info[0]['userAccountControl'][0]
  880. | AUTH_AD_ACCOUNTDISABLE;
  881. break;
  882. default:
  883. error ('auth: ldap user_disable() does not support selected usertype (..yet)');
  884. }
  885. $result = ldap_modify($ldapconnection, $userdn, $newinfo);
  886. $this->ldap_close();
  887. return $result;
  888. }*/
  889. /**
  890. * Returns true if user should be coursecreator.
  891. *
  892. * @param mixed $username username (without system magic quotes)
  893. * @return boolean result
  894. */
  895. function iscreator($username) {
  896. if (empty($this->config->creators) or empty($this->config->memberattribute)) {
  897. return null;
  898. }
  899. $textlib = textlib_get_instance();
  900. $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
  901. return (boolean)$this->ldap_isgroupmember($extusername, $this->config->creators);
  902. }
  903. /**
  904. * Called when the user record is updated.
  905. * Modifies user in external database. It takes olduser (before changes) and newuser (after changes)
  906. * conpares information saved modified information to external db.
  907. *
  908. * @param mixed $olduser Userobject before modifications (without system magic quotes)
  909. * @param mixed $newuser Userobject new modified userobject (without system magic quotes)
  910. * @return boolean result
  911. *
  912. */
  913. function user_update($olduser, $newuser) {
  914. global $USER;
  915. if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) {
  916. error_log("ERROR:User renaming not allowed in LDAP");
  917. return false;
  918. }
  919. if (isset($olduser->auth) and $olduser->auth != 'ldap') {
  920. return true; // just change auth and skip update
  921. }
  922. $attrmap = $this->ldap_attributes();
  923. // Before doing anything else, make sure really need to update anything
  924. // in the external LDAP server.
  925. $update_external = false;
  926. foreach ($attrmap as $key => $ldapkeys) {
  927. if (!empty($this->config->{'field_updateremote_'.$key})) {
  928. $update_external = true;
  929. break;
  930. }
  931. }
  932. if (!$update_external) {
  933. return true;
  934. }
  935. $textlib = textlib_get_instance();
  936. $extoldusername = $textlib->convert($olduser->username, 'utf-8', $this->config->ldapencoding);
  937. $ldapconnection = $this->ldap_connect();
  938. $search_attribs = array();
  939. foreach ($attrmap as $key => $values) {
  940. if (!is_array($values)) {
  941. $values = array($values);
  942. }
  943. foreach ($values as $value) {
  944. if (!in_array($value, $search_attribs)) {
  945. array_push($search_attribs, $value);
  946. }
  947. }
  948. }
  949. $user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername);
  950. $user_info_result = ldap_read($ldapconnection, $user_dn,
  951. $this->config->objectclass, $search_attribs);
  952. if ($user_info_result) {
  953. $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
  954. if (empty($user_entry)) {
  955. $error = 'ldap: Could not find user while updating externally. '.
  956. 'Details follow: search base: \''.$user_dn.'\'; search filter: \''.
  957. $this->config->objectclass.'\'; search attributes: ';
  958. foreach ($search_attribs as $attrib) {
  959. $error .= $attrib.' ';
  960. }
  961. error_log($error);
  962. return false; // old user not found!
  963. } else if (count($user_entry) > 1) {
  964. error_log('ldap: Strange! More than one user record found in ldap. Only using the first one.');
  965. return false;
  966. }
  967. $user_entry = $user_entry[0];
  968. //error_log(var_export($user_entry) . 'fpp' );
  969. foreach ($attrmap as $key => $ldapkeys) {
  970. // only process if the moodle field ($key) has changed and we
  971. // are set to update LDAP with it
  972. if (isset($olduser->$key) and isset($newuser->$key)
  973. and $olduser->$key !== $newuser->$key
  974. and !empty($this->config->{'field_updateremote_'. $key})) {
  975. // for ldap values that could be in more than one
  976. // ldap key, we will do our best to match
  977. // where they came from
  978. $ambiguous = true;
  979. $changed = false;
  980. if (!is_array($ldapkeys)) {
  981. $ldapkeys = array($ldapkeys);
  982. }
  983. if (count($ldapkeys) < 2) {
  984. $ambiguous = false;
  985. }
  986. $nuvalue = $textlib->convert($newuser->$key, 'utf-8', $this->config->ldapencoding);
  987. empty($nuvalue) ? $nuvalue = array() : $nuvalue;
  988. $ouvalue = $textlib->convert($olduser->$key, 'utf-8', $this->config->ldapencoding);
  989. foreach ($ldapkeys as $ldapkey) {
  990. $ldapkey = $ldapkey;
  991. $ldapvalue = $user_entry[$ldapkey][0];
  992. if (!$ambiguous) {
  993. // skip update if the values already match
  994. if ($nuvalue !== $ldapvalue) {
  995. //this might fail due to schema validation
  996. if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
  997. continue;
  998. } else {
  999. error_log('Error updating LDAP record. Error code: '
  1000. . ldap_errno($ldapconnection) . '; Error string : '
  1001. . ldap_err2str(ldap_errno($ldapconnection))
  1002. . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
  1003. continue;
  1004. }
  1005. }
  1006. } else {
  1007. // ambiguous
  1008. // value empty before in Moodle (and LDAP) - use 1st ldap candidate field
  1009. // no need to guess
  1010. if ($ouvalue === '') { // value empty before - use 1st ldap candidate
  1011. //this might fail due to schema validation
  1012. if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
  1013. $changed = true;
  1014. continue;
  1015. } else {
  1016. error_log('Error updating LDAP record. Error code: '
  1017. . ldap_errno($ldapconnection) . '; Error string : '
  1018. . ldap_err2str(ldap_errno($ldapconnection))
  1019. . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
  1020. continue;
  1021. }
  1022. }
  1023. // we found which ldap key to update!
  1024. if ($ouvalue !== '' and $ouvalue === $ldapvalue ) {
  1025. //this might fail due to schema validation
  1026. if (@ldap_modify($ldapconnection,

Large files files are truncated, but you can click here to view the full file