source: moodle/trunk/fuentes/auth/ldap/auth.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 95.9 KB
Line 
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Authentication Plugin: LDAP Authentication
19 * Authentication using LDAP (Lightweight Directory Access Protocol).
20 *
21 * @package auth_ldap
22 * @author Martin Dougiamas
23 * @author Iñaki Arenaza
24 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29// See http://support.microsoft.com/kb/305144 to interprete these values.
30if (!defined('AUTH_AD_ACCOUNTDISABLE')) {
31    define('AUTH_AD_ACCOUNTDISABLE', 0x0002);
32}
33if (!defined('AUTH_AD_NORMAL_ACCOUNT')) {
34    define('AUTH_AD_NORMAL_ACCOUNT', 0x0200);
35}
36if (!defined('AUTH_NTLMTIMEOUT')) {  // timewindow for the NTLM SSO process, in secs...
37    define('AUTH_NTLMTIMEOUT', 10);
38}
39
40// UF_DONT_EXPIRE_PASSWD value taken from MSDN directly
41if (!defined('UF_DONT_EXPIRE_PASSWD')) {
42    define ('UF_DONT_EXPIRE_PASSWD', 0x00010000);
43}
44
45// The Posix uid and gid of the 'nobody' account and 'nogroup' group.
46if (!defined('AUTH_UID_NOBODY')) {
47    define('AUTH_UID_NOBODY', -2);
48}
49if (!defined('AUTH_GID_NOGROUP')) {
50    define('AUTH_GID_NOGROUP', -2);
51}
52
53// Regular expressions for a valid NTLM username and domain name.
54if (!defined('AUTH_NTLM_VALID_USERNAME')) {
55    define('AUTH_NTLM_VALID_USERNAME', '[^/\\\\\\\\\[\]:;|=,+*?<>@"]+');
56}
57if (!defined('AUTH_NTLM_VALID_DOMAINNAME')) {
58    define('AUTH_NTLM_VALID_DOMAINNAME', '[^\\\\\\\\\/:*?"<>|]+');
59}
60// Default format for remote users if using NTLM SSO
61if (!defined('AUTH_NTLM_DEFAULT_FORMAT')) {
62    define('AUTH_NTLM_DEFAULT_FORMAT', '%domain%\\%username%');
63}
64if (!defined('AUTH_NTLM_FASTPATH_ATTEMPT')) {
65    define('AUTH_NTLM_FASTPATH_ATTEMPT', 0);
66}
67if (!defined('AUTH_NTLM_FASTPATH_YESFORM')) {
68    define('AUTH_NTLM_FASTPATH_YESFORM', 1);
69}
70if (!defined('AUTH_NTLM_FASTPATH_YESATTEMPT')) {
71    define('AUTH_NTLM_FASTPATH_YESATTEMPT', 2);
72}
73
74// Allows us to retrieve a diagnostic message in case of LDAP operation error
75if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
76    define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
77}
78
79require_once($CFG->libdir.'/authlib.php');
80require_once($CFG->libdir.'/ldaplib.php');
81require_once($CFG->dirroot.'/user/lib.php');
82
83/**
84 * LDAP authentication plugin.
85 */
86class auth_plugin_ldap extends auth_plugin_base {
87
88    /**
89     * Init plugin config from database settings depending on the plugin auth type.
90     */
91    function init_plugin($authtype) {
92        $this->pluginconfig = 'auth/'.$authtype;
93        $this->config = get_config($this->pluginconfig);
94        if (empty($this->config->ldapencoding)) {
95            $this->config->ldapencoding = 'utf-8';
96        }
97        if (empty($this->config->user_type)) {
98            $this->config->user_type = 'default';
99        }
100
101        $ldap_usertypes = ldap_supported_usertypes();
102        $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
103        unset($ldap_usertypes);
104
105        $default = ldap_getdefaults();
106
107        // Use defaults if values not given
108        foreach ($default as $key => $value) {
109            // watch out - 0, false are correct values too
110            if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
111                $this->config->{$key} = $value[$this->config->user_type];
112            }
113        }
114
115        // Hack prefix to objectclass
116        if (empty($this->config->objectclass)) {
117            // Can't send empty filter
118            $this->config->objectclass = '(objectClass=*)';
119        } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
120            // Value is 'objectClass=some-string-here', so just add ()
121            // around the value (filter _must_ have them).
122            $this->config->objectclass = '('.$this->config->objectclass.')';
123        } else if (strpos($this->config->objectclass, '(') !== 0) {
124            // Value is 'some-string-not-starting-with-left-parentheses',
125            // which is assumed to be the objectClass matching value.
126            // So build a valid filter with it.
127            $this->config->objectclass = '(objectClass='.$this->config->objectclass.')';
128        } else {
129            // There is an additional possible value
130            // '(some-string-here)', that can be used to specify any
131            // valid filter string, to select subsets of users based
132            // on any criteria. For example, we could select the users
133            // whose objectClass is 'user' and have the
134            // 'enabledMoodleUser' attribute, with something like:
135            //
136            //   (&(objectClass=user)(enabledMoodleUser=1))
137            //
138            // In this particular case we don't need to do anything,
139            // so leave $this->config->objectclass as is.
140        }
141    }
142
143    /**
144     * Constructor with initialisation.
145     */
146    public function __construct() {
147        $this->authtype = 'ldap';
148        $this->roleauth = 'auth_ldap';
149        $this->errorlogtag = '[AUTH LDAP] ';
150        $this->init_plugin($this->authtype);
151    }
152
153    /**
154     * Old syntax of class constructor for backward compatibility.
155     */
156    public function auth_plugin_ldap() {
157        self::__construct();
158    }
159
160    /**
161     * Returns true if the username and password work and false if they are
162     * wrong or don't exist.
163     *
164     * @param string $username The username (without system magic quotes)
165     * @param string $password The password (without system magic quotes)
166     *
167     * @return bool Authentication success or failure.
168     */
169    function user_login($username, $password) {
170        if (! function_exists('ldap_bind')) {
171            print_error('auth_ldapnotinstalled', 'auth_ldap');
172            return false;
173        }
174
175        if (!$username or !$password) {    // Don't allow blank usernames or passwords
176            return false;
177        }
178
179        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
180        $extpassword = core_text::convert($password, 'utf-8', $this->config->ldapencoding);
181
182        // Before we connect to LDAP, check if this is an AD SSO login
183        // if we succeed in this block, we'll return success early.
184        //
185        $key = sesskey();
186        if (!empty($this->config->ntlmsso_enabled) && $key === $password) {
187            $cf = get_cache_flags($this->pluginconfig.'/ntlmsess');
188            // We only get the cache flag if we retrieve it before
189            // it expires (AUTH_NTLMTIMEOUT seconds).
190            if (!isset($cf[$key]) || $cf[$key] === '') {
191                return false;
192            }
193
194            $sessusername = $cf[$key];
195            if ($username === $sessusername) {
196                unset($sessusername);
197                unset($cf);
198
199                // Check that the user is inside one of the configured LDAP contexts
200                $validuser = false;
201                $ldapconnection = $this->ldap_connect();
202                // if the user is not inside the configured contexts,
203                // ldap_find_userdn returns false.
204                if ($this->ldap_find_userdn($ldapconnection, $extusername)) {
205                    $validuser = true;
206                }
207                $this->ldap_close();
208
209                // Shortcut here - SSO confirmed
210                return $validuser;
211            }
212        } // End SSO processing
213        unset($key);
214
215        $ldapconnection = $this->ldap_connect();
216        $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
217
218        // If ldap_user_dn is empty, user does not exist
219        if (!$ldap_user_dn) {
220            $this->ldap_close();
221            return false;
222        }
223
224        // Try to bind with current username and password
225        $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword);
226
227        // If login fails and we are using MS Active Directory, retrieve the diagnostic
228        // message to see if this is due to an expired password, or that the user is forced to
229        // change the password on first login. If it is, only proceed if we can change
230        // password from Moodle (otherwise we'll get stuck later in the login process).
231        if (!$ldap_login && ($this->config->user_type == 'ad')
232            && $this->can_change_password()
233            && (!empty($this->config->expiration) and ($this->config->expiration == 1))) {
234
235            // We need to get the diagnostic message right after the call to ldap_bind(),
236            // before any other LDAP operation.
237            ldap_get_option($ldapconnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagmsg);
238
239            if ($this->ldap_ad_pwdexpired_from_diagmsg($diagmsg)) {
240                // If login failed because user must change the password now or the
241                // password has expired, let the user in. We'll catch this later in the
242                // login process when we explicitly check for expired passwords.
243                $ldap_login = true;
244            }
245        }
246        $this->ldap_close();
247        return $ldap_login;
248    }
249
250    /**
251     * Reads user information from ldap and returns it in array()
252     *
253     * Function should return all information available. If you are saving
254     * this information to moodle user-table you should honor syncronization flags
255     *
256     * @param string $username username
257     *
258     * @return mixed array with no magic quotes or false on error
259     */
260    function get_userinfo($username) {
261        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
262
263        $ldapconnection = $this->ldap_connect();
264        if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) {
265            $this->ldap_close();
266            return false;
267        }
268
269        $search_attribs = array();
270        $attrmap = $this->ldap_attributes();
271        foreach ($attrmap as $key => $values) {
272            if (!is_array($values)) {
273                $values = array($values);
274            }
275            foreach ($values as $value) {
276                if (!in_array($value, $search_attribs)) {
277                    array_push($search_attribs, $value);
278                }
279            }
280        }
281
282        if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) {
283            $this->ldap_close();
284            return false; // error!
285        }
286
287        $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result);
288        if (empty($user_entry)) {
289            $this->ldap_close();
290            return false; // entry not found
291        }
292
293        $result = array();
294        foreach ($attrmap as $key => $values) {
295            if (!is_array($values)) {
296                $values = array($values);
297            }
298            $ldapval = NULL;
299            foreach ($values as $value) {
300                $entry = array_change_key_case($user_entry[0], CASE_LOWER);
301                if (($value == 'dn') || ($value == 'distinguishedname')) {
302                    $result[$key] = $user_dn;
303                    continue;
304                }
305                if (!array_key_exists($value, $entry)) {
306                    continue; // wrong data mapping!
307                }
308                if (is_array($entry[$value])) {
309                    $newval = core_text::convert($entry[$value][0], $this->config->ldapencoding, 'utf-8');
310                } else {
311                    $newval = core_text::convert($entry[$value], $this->config->ldapencoding, 'utf-8');
312                }
313                if (!empty($newval)) { // favour ldap entries that are set
314                    $ldapval = $newval;
315                }
316            }
317            if (!is_null($ldapval)) {
318                $result[$key] = $ldapval;
319            }
320        }
321
322        $this->ldap_close();
323        return $result;
324    }
325
326    /**
327     * Reads user information from ldap and returns it in an object
328     *
329     * @param string $username username (with system magic quotes)
330     * @return mixed object or false on error
331     */
332    function get_userinfo_asobj($username) {
333        $user_array = $this->get_userinfo($username);
334        if ($user_array == false) {
335            return false; //error or not found
336        }
337        $user_array = truncate_userinfo($user_array);
338        $user = new stdClass();
339        foreach ($user_array as $key=>$value) {
340            $user->{$key} = $value;
341        }
342        return $user;
343    }
344
345    /**
346     * Returns all usernames from LDAP
347     *
348     * get_userlist returns all usernames from LDAP
349     *
350     * @return array
351     */
352    function get_userlist() {
353        return $this->ldap_get_userlist("({$this->config->user_attribute}=*)");
354    }
355
356    /**
357     * Checks if user exists on LDAP
358     *
359     * @param string $username
360     */
361    function user_exists($username) {
362        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
363
364        // Returns true if given username exists on ldap
365        $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')');
366        return count($users);
367    }
368
369    /**
370     * Creates a new user on LDAP.
371     * By using information in userobject
372     * Use user_exists to prevent duplicate usernames
373     *
374     * @param mixed $userobject  Moodle userobject
375     * @param mixed $plainpass   Plaintext password
376     */
377    function user_create($userobject, $plainpass) {
378        $extusername = core_text::convert($userobject->username, 'utf-8', $this->config->ldapencoding);
379        $extpassword = core_text::convert($plainpass, 'utf-8', $this->config->ldapencoding);
380
381        switch ($this->config->passtype) {
382            case 'md5':
383                $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
384                break;
385            case 'sha1':
386                $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
387                break;
388            case 'plaintext':
389            default:
390                break; // plaintext
391        }
392
393        $ldapconnection = $this->ldap_connect();
394        $attrmap = $this->ldap_attributes();
395
396        $newuser = array();
397
398        foreach ($attrmap as $key => $values) {
399            if (!is_array($values)) {
400                $values = array($values);
401            }
402            foreach ($values as $value) {
403                if (!empty($userobject->$key) ) {
404                    $newuser[$value] = core_text::convert($userobject->$key, 'utf-8', $this->config->ldapencoding);
405                }
406            }
407        }
408
409        //Following sets all mandatory and other forced attribute values
410        //User should be creted as login disabled untill email confirmation is processed
411        //Feel free to add your user type and send patches to paca@sci.fi to add them
412        //Moodle distribution
413
414        switch ($this->config->user_type)  {
415            case 'edir':
416                $newuser['objectClass']   = array('inetOrgPerson', 'organizationalPerson', 'person', 'top');
417                $newuser['uniqueId']      = $extusername;
418                $newuser['logindisabled'] = 'TRUE';
419                $newuser['userpassword']  = $extpassword;
420                $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser);
421                break;
422            case 'rfc2307':
423            case 'rfc2307bis':
424                // posixAccount object class forces us to specify a uidNumber
425                // and a gidNumber. That is quite complicated to generate from
426                // Moodle without colliding with existing numbers and without
427                // race conditions. As this user is supposed to be only used
428                // with Moodle (otherwise the user would exist beforehand) and
429                // doesn't need to login into a operating system, we assign the
430                // user the uid of user 'nobody' and gid of group 'nogroup'. In
431                // addition to that, we need to specify a home directory. We
432                // use the root directory ('/') as the home directory, as this
433                // is the only one can always be sure exists. Finally, even if
434                // it's not mandatory, we specify '/bin/false' as the login
435                // shell, to prevent the user from login in at the operating
436                // system level (Moodle ignores this).
437
438                $newuser['objectClass']   = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top');
439                $newuser['cn']            = $extusername;
440                $newuser['uid']           = $extusername;
441                $newuser['uidNumber']     = AUTH_UID_NOBODY;
442                $newuser['gidNumber']     = AUTH_GID_NOGROUP;
443                $newuser['homeDirectory'] = '/';
444                $newuser['loginShell']    = '/bin/false';
445
446                // IMPORTANT:
447                // We have to create the account locked, but posixAccount has
448                // no attribute to achive this reliably. So we are going to
449                // modify the password in a reversable way that we can later
450                // revert in user_activate().
451                //
452                // Beware that this can be defeated by the user if we are not
453                // using MD5 or SHA-1 passwords. After all, the source code of
454                // Moodle is available, and the user can see the kind of
455                // modification we are doing and 'undo' it by hand (but only
456                // if we are using plain text passwords).
457                //
458                // Also bear in mind that you need to use a binding user that
459                // can create accounts and has read/write privileges on the
460                // 'userPassword' attribute for this to work.
461
462                $newuser['userPassword']  = '*'.$extpassword;
463                $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser);
464                break;
465            case 'ad':
466                // User account creation is a two step process with AD. First you
467                // create the user object, then you set the password. If you try
468                // to set the password while creating the user, the operation
469                // fails.
470
471                // Passwords in Active Directory must be encoded as Unicode
472                // strings (UCS-2 Little Endian format) and surrounded with
473                // double quotes. See http://support.microsoft.com/?kbid=269190
474                if (!function_exists('mb_convert_encoding')) {
475                    print_error('auth_ldap_no_mbstring', 'auth_ldap');
476                }
477
478                // Check for invalid sAMAccountName characters.
479                if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) {
480                    print_error ('auth_ldap_ad_invalidchars', 'auth_ldap');
481                }
482
483                // First create the user account, and mark it as disabled.
484                $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson');
485                $newuser['sAMAccountName'] = $extusername;
486                $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT |
487                                                 AUTH_AD_ACCOUNTDISABLE;
488                $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context;
489                if (!ldap_add($ldapconnection, $userdn, $newuser)) {
490                    print_error('auth_ldap_ad_create_req', 'auth_ldap');
491                }
492
493                // Now set the password
494                unset($newuser);
495                $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"',
496                                                             'UCS-2LE', 'UTF-8');
497                if(!ldap_modify($ldapconnection, $userdn, $newuser)) {
498                    // Something went wrong: delete the user account and error out
499                    ldap_delete ($ldapconnection, $userdn);
500                    print_error('auth_ldap_ad_create_req', 'auth_ldap');
501                }
502                $uadd = true;
503                break;
504            default:
505               print_error('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name);
506        }
507        $this->ldap_close();
508        return $uadd;
509    }
510
511    /**
512     * Returns true if plugin allows resetting of password from moodle.
513     *
514     * @return bool
515     */
516    function can_reset_password() {
517        return !empty($this->config->stdchangepassword);
518    }
519
520    /**
521     * Returns true if plugin can be manually set.
522     *
523     * @return bool
524     */
525    function can_be_manually_set() {
526        return true;
527    }
528
529    /**
530     * Returns true if plugin allows signup and user creation.
531     *
532     * @return bool
533     */
534    function can_signup() {
535        return (!empty($this->config->auth_user_create) and !empty($this->config->create_context));
536    }
537
538    /**
539     * Sign up a new user ready for confirmation.
540     * Password is passed in plaintext.
541     *
542     * @param object $user new user object
543     * @param boolean $notify print notice with link and terminate
544     * @return boolean success
545     */
546    function user_signup($user, $notify=true) {
547        global $CFG, $DB, $PAGE, $OUTPUT;
548
549        require_once($CFG->dirroot.'/user/profile/lib.php');
550        require_once($CFG->dirroot.'/user/lib.php');
551
552        if ($this->user_exists($user->username)) {
553            print_error('auth_ldap_user_exists', 'auth_ldap');
554        }
555
556        $plainslashedpassword = $user->password;
557        unset($user->password);
558
559        if (! $this->user_create($user, $plainslashedpassword)) {
560            print_error('auth_ldap_create_error', 'auth_ldap');
561        }
562
563        $user->id = user_create_user($user, false, false);
564
565        user_add_password_history($user->id, $plainslashedpassword);
566
567        // Save any custom profile field information
568        profile_save_data($user);
569
570        $this->update_user_record($user->username);
571        // This will also update the stored hash to the latest algorithm
572        // if the existing hash is using an out-of-date algorithm (or the
573        // legacy md5 algorithm).
574        update_internal_user_password($user, $plainslashedpassword);
575
576        $user = $DB->get_record('user', array('id'=>$user->id));
577
578        \core\event\user_created::create_from_userid($user->id)->trigger();
579
580        if (! send_confirmation_email($user)) {
581            print_error('noemail', 'auth_ldap');
582        }
583
584        if ($notify) {
585            $emailconfirm = get_string('emailconfirm');
586            $PAGE->set_url('/auth/ldap/auth.php');
587            $PAGE->navbar->add($emailconfirm);
588            $PAGE->set_title($emailconfirm);
589            $PAGE->set_heading($emailconfirm);
590            echo $OUTPUT->header();
591            notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php");
592        } else {
593            return true;
594        }
595    }
596
597    /**
598     * Returns true if plugin allows confirming of new users.
599     *
600     * @return bool
601     */
602    function can_confirm() {
603        return $this->can_signup();
604    }
605
606    /**
607     * Confirm the new user as registered.
608     *
609     * @param string $username
610     * @param string $confirmsecret
611     */
612    function user_confirm($username, $confirmsecret) {
613        global $DB;
614
615        $user = get_complete_user_data('username', $username);
616
617        if (!empty($user)) {
618            if ($user->auth != $this->authtype) {
619                return AUTH_CONFIRM_ERROR;
620
621            } else if ($user->secret == $confirmsecret && $user->confirmed) {
622                return AUTH_CONFIRM_ALREADY;
623
624            } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in
625                if (!$this->user_activate($username)) {
626                    return AUTH_CONFIRM_FAIL;
627                }
628                $user->confirmed = 1;
629                user_update_user($user, false);
630                return AUTH_CONFIRM_OK;
631            }
632        } else {
633            return AUTH_CONFIRM_ERROR;
634        }
635    }
636
637    /**
638     * Return number of days to user password expires
639     *
640     * If userpassword does not expire it should return 0. If password is already expired
641     * it should return negative value.
642     *
643     * @param mixed $username username
644     * @return integer
645     */
646    function password_expire($username) {
647        $result = 0;
648
649        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
650
651        $ldapconnection = $this->ldap_connect();
652        $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
653        $search_attribs = array($this->config->expireattr);
654        $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
655        if ($sr)  {
656            $info = ldap_get_entries_moodle($ldapconnection, $sr);
657            if (!empty ($info)) {
658                $info = array_change_key_case($info[0], CASE_LOWER);
659                if (isset($info[$this->config->expireattr][0])) {
660                    $expiretime = $this->ldap_expirationtime2unix($info[$this->config->expireattr][0], $ldapconnection, $user_dn);
661                    if ($expiretime != 0) {
662                        $now = time();
663                        if ($expiretime > $now) {
664                            $result = ceil(($expiretime - $now) / DAYSECS);
665                        } else {
666                            $result = floor(($expiretime - $now) / DAYSECS);
667                        }
668                    }
669                }
670            }
671        } else {
672            error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap'));
673        }
674
675        return $result;
676    }
677
678    /**
679     * Syncronizes user fron external LDAP server to moodle user table
680     *
681     * Sync is now using username attribute.
682     *
683     * Syncing users removes or suspends users that dont exists anymore in external LDAP.
684     * Creates new users and updates coursecreator status of users.
685     *
686     * @param bool $do_updates will do pull in data updates from LDAP if relevant
687     */
688    function sync_users($do_updates=true) {
689        global $CFG, $DB;
690
691        print_string('connectingldap', 'auth_ldap');
692        $ldapconnection = $this->ldap_connect();
693
694        $dbman = $DB->get_manager();
695
696    /// Define table user to be created
697        $table = new xmldb_table('tmp_extuser');
698        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
699        $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
700        $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
701        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
702        $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username'));
703
704        print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser');
705        $dbman->create_temp_table($table);
706
707        ////
708        //// get user's list from ldap to sql in a scalable fashion
709        ////
710        // prepare some data we'll need
711        $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
712
713        $contexts = explode(';', $this->config->contexts);
714
715        if (!empty($this->config->create_context)) {
716            array_push($contexts, $this->config->create_context);
717        }
718
719        $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version);
720        $ldap_cookie = '';
721        foreach ($contexts as $context) {
722            $context = trim($context);
723            if (empty($context)) {
724                continue;
725            }
726
727            do {
728                if ($ldap_pagedresults) {
729                    ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
730                }
731                if ($this->config->search_sub) {
732                    // Use ldap_search to find first user from subtree.
733                    $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
734                } else {
735                    // Search only in this context.
736                    $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
737                }
738                if(!$ldap_result) {
739                    continue;
740                }
741                if ($ldap_pagedresults) {
742                    ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
743                }
744                if ($entry = @ldap_first_entry($ldapconnection, $ldap_result)) {
745                    do {
746                        $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute);
747                        $value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8');
748                        $value = trim($value);
749                        $this->ldap_bulk_insert($value);
750                    } while ($entry = ldap_next_entry($ldapconnection, $entry));
751                }
752                unset($ldap_result); // Free mem.
753            } while ($ldap_pagedresults && $ldap_cookie !== null && $ldap_cookie != '');
754        }
755
756        // If LDAP paged results were used, the current connection must be completely
757        // closed and a new one created, to work without paged results from here on.
758        if ($ldap_pagedresults) {
759            $this->ldap_close(true);
760            $ldapconnection = $this->ldap_connect();
761        }
762
763        /// preserve our user database
764        /// if the temp table is empty, it probably means that something went wrong, exit
765        /// so as to avoid mass deletion of users; which is hard to undo
766        $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}');
767        if ($count < 1) {
768            print_string('didntgetusersfromldap', 'auth_ldap');
769            exit;
770        } else {
771            print_string('gotcountrecordsfromldap', 'auth_ldap', $count);
772        }
773
774
775/// User removal
776        // Find users in DB that aren't in ldap -- to be removed!
777        // this is still not as scalable (but how often do we mass delete?)
778
779        if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) {
780            $sql = "SELECT u.*
781                      FROM {user} u
782                 LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
783                     WHERE u.auth = :auth
784                           AND u.deleted = 0
785                           AND e.username IS NULL";
786            $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype));
787
788            if (!empty($remove_users)) {
789                print_string('userentriestoremove', 'auth_ldap', count($remove_users));
790                foreach ($remove_users as $user) {
791                    if (delete_user($user)) {
792                        echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
793                    } else {
794                        echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n";
795                    }
796                }
797            } else {
798                print_string('nouserentriestoremove', 'auth_ldap');
799            }
800            unset($remove_users); // Free mem!
801
802        } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
803            $sql = "SELECT u.*
804                      FROM {user} u
805                 LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
806                     WHERE u.auth = :auth
807                           AND u.deleted = 0
808                           AND u.suspended = 0
809                           AND e.username IS NULL";
810            $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype));
811
812            if (!empty($remove_users)) {
813                print_string('userentriestoremove', 'auth_ldap', count($remove_users));
814
815                foreach ($remove_users as $user) {
816                    $updateuser = new stdClass();
817                    $updateuser->id = $user->id;
818                    $updateuser->suspended = 1;
819                    user_update_user($updateuser, false);
820                    echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
821                    \core\session\manager::kill_user_sessions($user->id);
822                }
823            } else {
824                print_string('nouserentriestoremove', 'auth_ldap');
825            }
826            unset($remove_users); // Free mem!
827        }
828
829/// Revive suspended users
830        if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
831            $sql = "SELECT u.id, u.username
832                      FROM {user} u
833                      JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
834                     WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0";
835            // Note: 'nologin' is there for backwards compatibility.
836            $revive_users = $DB->get_records_sql($sql, array($this->authtype));
837
838            if (!empty($revive_users)) {
839                print_string('userentriestorevive', 'auth_ldap', count($revive_users));
840
841                foreach ($revive_users as $user) {
842                    $updateuser = new stdClass();
843                    $updateuser->id = $user->id;
844                    $updateuser->auth = $this->authtype;
845                    $updateuser->suspended = 0;
846                    user_update_user($updateuser, false);
847                    echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
848                }
849            } else {
850                print_string('nouserentriestorevive', 'auth_ldap');
851            }
852
853            unset($revive_users);
854        }
855
856
857/// User Updates - time-consuming (optional)
858        if ($do_updates) {
859            // Narrow down what fields we need to update
860            $all_keys = array_keys(get_object_vars($this->config));
861            $updatekeys = array();
862            foreach ($all_keys as $key) {
863                if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) {
864                    // If we have a field to update it from
865                    // and it must be updated 'onlogin' we
866                    // update it on cron
867                    if (!empty($this->config->{'field_map_'.$match[1]})
868                         and $this->config->{$match[0]} === 'onlogin') {
869                        array_push($updatekeys, $match[1]); // the actual key name
870                    }
871                }
872            }
873            unset($all_keys); unset($key);
874
875        } else {
876            print_string('noupdatestobedone', 'auth_ldap');
877        }
878        if ($do_updates and !empty($updatekeys)) { // run updates only if relevant
879            $users = $DB->get_records_sql('SELECT u.username, u.id
880                                             FROM {user} u
881                                            WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?',
882                                          array($this->authtype, $CFG->mnet_localhost_id));
883            if (!empty($users)) {
884                print_string('userentriestoupdate', 'auth_ldap', count($users));
885
886                $sitecontext = context_system::instance();
887                if (!empty($this->config->creators) and !empty($this->config->memberattribute)
888                  and $roles = get_archetype_roles('coursecreator')) {
889                    $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
890                } else {
891                    $creatorrole = false;
892                }
893
894                $transaction = $DB->start_delegated_transaction();
895                $xcount = 0;
896                $maxxcount = 100;
897
898                foreach ($users as $user) {
899                    echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id));
900                    if (!$this->update_user_record($user->username, $updatekeys, true)) {
901                        echo ' - '.get_string('skipped');
902                    }
903                    echo "\n";
904                    $xcount++;
905
906                    // Update course creators if needed
907                    if ($creatorrole !== false) {
908                        if ($this->iscreator($user->username)) {
909                            role_assign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth);
910                        } else {
911                            role_unassign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth);
912                        }
913                    }
914                }
915                $transaction->allow_commit();
916                unset($users); // free mem
917            }
918        } else { // end do updates
919            print_string('noupdatestobedone', 'auth_ldap');
920        }
921
922/// User Additions
923        // Find users missing in DB that are in LDAP
924        // and gives me a nifty object I don't want.
925        // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin
926        $sql = 'SELECT e.id, e.username
927                  FROM {tmp_extuser} e
928                  LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid)
929                 WHERE u.id IS NULL';
930        $add_users = $DB->get_records_sql($sql);
931
932        if (!empty($add_users)) {
933            print_string('userentriestoadd', 'auth_ldap', count($add_users));
934
935            $sitecontext = context_system::instance();
936            if (!empty($this->config->creators) and !empty($this->config->memberattribute)
937              and $roles = get_archetype_roles('coursecreator')) {
938                $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
939            } else {
940                $creatorrole = false;
941            }
942
943            $transaction = $DB->start_delegated_transaction();
944            foreach ($add_users as $user) {
945                $user = $this->get_userinfo_asobj($user->username);
946
947                // Prep a few params
948                $user->modified   = time();
949                $user->confirmed  = 1;
950                $user->auth       = $this->authtype;
951                $user->mnethostid = $CFG->mnet_localhost_id;
952                // get_userinfo_asobj() might have replaced $user->username with the value
953                // from the LDAP server (which can be mixed-case). Make sure it's lowercase
954                $user->username = trim(core_text::strtolower($user->username));
955                if (empty($user->lang)) {
956                    $user->lang = $CFG->lang;
957                }
958                if (empty($user->calendartype)) {
959                    $user->calendartype = $CFG->calendartype;
960                }
961
962                $id = user_create_user($user, false);
963                echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
964                $euser = $DB->get_record('user', array('id' => $id));
965
966                if (!empty($this->config->forcechangepassword)) {
967                    set_user_preference('auth_forcepasswordchange', 1, $id);
968                }
969
970                // Add course creators if needed
971                if ($creatorrole !== false and $this->iscreator($user->username)) {
972                    role_assign($creatorrole->id, $id, $sitecontext->id, $this->roleauth);
973                }
974
975            }
976            $transaction->allow_commit();
977            unset($add_users); // free mem
978        } else {
979            print_string('nouserstobeadded', 'auth_ldap');
980        }
981
982        $dbman->drop_table($table);
983        $this->ldap_close();
984
985        return true;
986    }
987
988    /**
989     * Update a local user record from an external source.
990     * This is a lighter version of the one in moodlelib -- won't do
991     * expensive ops such as enrolment.
992     *
993     * If you don't pass $updatekeys, there is a performance hit and
994     * values removed from LDAP won't be removed from moodle.
995     *
996     * @param string $username username
997     * @param boolean $updatekeys true to update the local record with the external LDAP values.
998     * @param bool $triggerevent set false if user_updated event should not be triggered.
999     *             This will not affect user_password_updated event triggering.
1000     * @return stdClass|bool updated user record or false if there is no new info to update.
1001     */
1002    function update_user_record($username, $updatekeys = false, $triggerevent = false) {
1003        global $CFG, $DB;
1004
1005        // Just in case check text case
1006        $username = trim(core_text::strtolower($username));
1007
1008        // Get the current user record
1009        $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id));
1010        if (empty($user)) { // trouble
1011            error_log($this->errorlogtag.get_string('auth_dbusernotexist', 'auth_db', '', $username));
1012            print_error('auth_dbusernotexist', 'auth_db', '', $username);
1013            die;
1014        }
1015
1016        // Protect the userid from being overwritten
1017        $userid = $user->id;
1018
1019        if ($newinfo = $this->get_userinfo($username)) {
1020            $newinfo = truncate_userinfo($newinfo);
1021
1022            if (empty($updatekeys)) { // all keys? this does not support removing values
1023                $updatekeys = array_keys($newinfo);
1024            }
1025
1026            if (!empty($updatekeys)) {
1027                $newuser = new stdClass();
1028                $newuser->id = $userid;
1029
1030                foreach ($updatekeys as $key) {
1031                    if (isset($newinfo[$key])) {
1032                        $value = $newinfo[$key];
1033                    } else {
1034                        $value = '';
1035                    }
1036
1037                    if (!empty($this->config->{'field_updatelocal_' . $key})) {
1038                        // Only update if it's changed.
1039                        if ($user->{$key} != $value) {
1040                            $newuser->$key = $value;
1041                        }
1042                    }
1043                }
1044                user_update_user($newuser, false, $triggerevent);
1045            }
1046        } else {
1047            return false;
1048        }
1049        return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
1050    }
1051
1052    /**
1053     * Bulk insert in SQL's temp table
1054     */
1055    function ldap_bulk_insert($username) {
1056        global $DB, $CFG;
1057
1058        $username = core_text::strtolower($username); // usernames are __always__ lowercase.
1059        $DB->insert_record_raw('tmp_extuser', array('username'=>$username,
1060                                                    'mnethostid'=>$CFG->mnet_localhost_id), false, true);
1061        echo '.';
1062    }
1063
1064    /**
1065     * Activates (enables) user in external LDAP so user can login
1066     *
1067     * @param mixed $username
1068     * @return boolean result
1069     */
1070    function user_activate($username) {
1071        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
1072
1073        $ldapconnection = $this->ldap_connect();
1074
1075        $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
1076        switch ($this->config->user_type)  {
1077            case 'edir':
1078                $newinfo['loginDisabled'] = 'FALSE';
1079                break;
1080            case 'rfc2307':
1081            case 'rfc2307bis':
1082                // Remember that we add a '*' character in front of the
1083                // external password string to 'disable' the account. We just
1084                // need to remove it.
1085                $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)',
1086                                array('userPassword'));
1087                $info = ldap_get_entries($ldapconnection, $sr);
1088                $info[0] = array_change_key_case($info[0], CASE_LOWER);
1089                $newinfo['userPassword'] = ltrim($info[0]['userpassword'][0], '*');
1090                break;
1091            case 'ad':
1092                // We need to unset the ACCOUNTDISABLE bit in the
1093                // userAccountControl attribute ( see
1094                // http://support.microsoft.com/kb/305144 )
1095                $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)',
1096                                array('userAccountControl'));
1097                $info = ldap_get_entries($ldapconnection, $sr);
1098                $info[0] = array_change_key_case($info[0], CASE_LOWER);
1099                $newinfo['userAccountControl'] = $info[0]['useraccountcontrol'][0]
1100                                                 & (~AUTH_AD_ACCOUNTDISABLE);
1101                break;
1102            default:
1103                print_error('user_activatenotsupportusertype', 'auth_ldap', '', $this->config->user_type_name);
1104        }
1105        $result = ldap_modify($ldapconnection, $userdn, $newinfo);
1106        $this->ldap_close();
1107        return $result;
1108    }
1109
1110    /**
1111     * Returns true if user should be coursecreator.
1112     *
1113     * @param mixed $username    username (without system magic quotes)
1114     * @return mixed result      null if course creators is not configured, boolean otherwise.
1115     */
1116    function iscreator($username) {
1117        if (empty($this->config->creators) or empty($this->config->memberattribute)) {
1118            return null;
1119        }
1120
1121        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
1122
1123        $ldapconnection = $this->ldap_connect();
1124
1125        if ($this->config->memberattribute_isdn) {
1126            if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) {
1127                return false;
1128            }
1129        } else {
1130            $userid = $extusername;
1131        }
1132
1133        $group_dns = explode(';', $this->config->creators);
1134        $creator = ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute);
1135
1136        $this->ldap_close();
1137
1138        return $creator;
1139    }
1140
1141    /**
1142     * Called when the user record is updated.
1143     *
1144     * Modifies user in external LDAP server. It takes olduser (before
1145     * changes) and newuser (after changes) compares information and
1146     * saves modified information to external LDAP server.
1147     *
1148     * @param mixed $olduser     Userobject before modifications    (without system magic quotes)
1149     * @param mixed $newuser     Userobject new modified userobject (without system magic quotes)
1150     * @return boolean result
1151     *
1152     */
1153    function user_update($olduser, $newuser) {
1154        global $USER;
1155
1156        if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) {
1157            error_log($this->errorlogtag.get_string('renamingnotallowed', 'auth_ldap'));
1158            return false;
1159        }
1160
1161        if (isset($olduser->auth) and $olduser->auth != $this->authtype) {
1162            return true; // just change auth and skip update
1163        }
1164
1165        $attrmap = $this->ldap_attributes();
1166        // Before doing anything else, make sure we really need to update anything
1167        // in the external LDAP server.
1168        $update_external = false;
1169        foreach ($attrmap as $key => $ldapkeys) {
1170            if (!empty($this->config->{'field_updateremote_'.$key})) {
1171                $update_external = true;
1172                break;
1173            }
1174        }
1175        if (!$update_external) {
1176            return true;
1177        }
1178
1179        $extoldusername = core_text::convert($olduser->username, 'utf-8', $this->config->ldapencoding);
1180
1181        $ldapconnection = $this->ldap_connect();
1182
1183        $search_attribs = array();
1184        foreach ($attrmap as $key => $values) {
1185            if (!is_array($values)) {
1186                $values = array($values);
1187            }
1188            foreach ($values as $value) {
1189                if (!in_array($value, $search_attribs)) {
1190                    array_push($search_attribs, $value);
1191                }
1192            }
1193        }
1194
1195        if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername))) {
1196            return false;
1197        }
1198
1199        $success = true;
1200        $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
1201        if ($user_info_result) {
1202            $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result);
1203            if (empty($user_entry)) {
1204                $attribs = join (', ', $search_attribs);
1205                error_log($this->errorlogtag.get_string('updateusernotfound', 'auth_ldap',
1206                                                          array('userdn'=>$user_dn,
1207                                                                'attribs'=>$attribs)));
1208                return false; // old user not found!
1209            } else if (count($user_entry) > 1) {
1210                error_log($this->errorlogtag.get_string('morethanoneuser', 'auth_ldap'));
1211                return false;
1212            }
1213
1214            $user_entry = array_change_key_case($user_entry[0], CASE_LOWER);
1215
1216            foreach ($attrmap as $key => $ldapkeys) {
1217                $profilefield = '';
1218                // Only process if the moodle field ($key) has changed and we
1219                // are set to update LDAP with it
1220                $customprofilefield = 'profile_field_' . $key;
1221                if (isset($olduser->$key) and isset($newuser->$key)
1222                    and ($olduser->$key !== $newuser->$key)) {
1223                    $profilefield = $key;
1224                } else if (isset($olduser->$customprofilefield) && isset($newuser->$customprofilefield)
1225                    && $olduser->$customprofilefield !== $newuser->$customprofilefield) {
1226                    $profilefield = $customprofilefield;
1227                }
1228
1229                if (!empty($profilefield) && !empty($this->config->{'field_updateremote_' . $key})) {
1230                    // For ldap values that could be in more than one
1231                    // ldap key, we will do our best to match
1232                    // where they came from
1233                    $ambiguous = true;
1234                    $changed   = false;
1235                    if (!is_array($ldapkeys)) {
1236                        $ldapkeys = array($ldapkeys);
1237                    }
1238                    if (count($ldapkeys) < 2) {
1239                        $ambiguous = false;
1240                    }
1241
1242                    $nuvalue = core_text::convert($newuser->$profilefield, 'utf-8', $this->config->ldapencoding);
1243                    empty($nuvalue) ? $nuvalue = array() : $nuvalue;
1244                    $ouvalue = core_text::convert($olduser->$profilefield, 'utf-8', $this->config->ldapencoding);
1245
1246                    foreach ($ldapkeys as $ldapkey) {
1247                        $ldapkey   = $ldapkey;
1248                        $ldapvalue = $user_entry[$ldapkey][0];
1249                        if (!$ambiguous) {
1250                            // Skip update if the values already match
1251                            if ($nuvalue !== $ldapvalue) {
1252                                // This might fail due to schema validation
1253                                if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
1254                                    $changed = true;
1255                                    continue;
1256                                } else {
1257                                    $success = false;
1258                                    error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap',
1259                                                                             array('errno'=>ldap_errno($ldapconnection),
1260                                                                                   'errstring'=>ldap_err2str(ldap_errno($ldapconnection)),
1261                                                                                   'key'=>$key,
1262                                                                                   'ouvalue'=>$ouvalue,
1263                                                                                   'nuvalue'=>$nuvalue)));
1264                                    continue;
1265                                }
1266                            }
1267                        } else {
1268                            // Ambiguous. Value empty before in Moodle (and LDAP) - use
1269                            // 1st ldap candidate field, no need to guess
1270                            if ($ouvalue === '') { // value empty before - use 1st ldap candidate
1271                                // This might fail due to schema validation
1272                                if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
1273                                    $changed = true;
1274                                    continue;
1275                                } else {
1276                                    $success = false;
1277                                    error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap',
1278                                                                             array('errno'=>ldap_errno($ldapconnection),
1279                                                                                   'errstring'=>ldap_err2str(ldap_errno($ldapconnection)),
1280                                                                                   'key'=>$key,
1281                                                                                   'ouvalue'=>$ouvalue,
1282                                                                                   'nuvalue'=>$nuvalue)));
1283                                    continue;
1284                                }
1285                            }
1286
1287                            // We found which ldap key to update!
1288                            if ($ouvalue !== '' and $ouvalue === $ldapvalue ) {
1289                                // This might fail due to schema validation
1290                                if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
1291                                    $changed = true;
1292                                    continue;
1293                                } else {
1294                                    $success = false;
1295                                    error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap',
1296                                                                             array('errno'=>ldap_errno($ldapconnection),
1297                                                                                   'errstring'=>ldap_err2str(ldap_errno($ldapconnection)),
1298                                                                                   'key'=>$key,
1299                                                                                   'ouvalue'=>$ouvalue,
1300                                                                                   'nuvalue'=>$nuvalue)));
1301                                    continue;
1302                                }
1303                            }
1304                        }
1305                    }
1306
1307                    if ($ambiguous and !$changed) {
1308                        $success = false;
1309                        error_log($this->errorlogtag.get_string ('updateremfailamb', 'auth_ldap',
1310                                                                 array('key'=>$key,
1311                                                                       'ouvalue'=>$ouvalue,
1312                                                                       'nuvalue'=>$nuvalue)));
1313                    }
1314                }
1315            }
1316        } else {
1317            error_log($this->errorlogtag.get_string ('usernotfound', 'auth_ldap'));
1318            $success = false;
1319        }
1320
1321        $this->ldap_close();
1322        return $success;
1323
1324    }
1325
1326    /**
1327     * Changes userpassword in LDAP
1328     *
1329     * Called when the user password is updated. It assumes it is
1330     * called by an admin or that you've otherwise checked the user's
1331     * credentials
1332     *
1333     * @param  object  $user        User table object
1334     * @param  string  $newpassword Plaintext password (not crypted/md5'ed)
1335     * @return boolean result
1336     *
1337     */
1338    function user_update_password($user, $newpassword) {
1339        global $USER;
1340
1341        $result = false;
1342        $username = $user->username;
1343
1344        $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding);
1345        $extpassword = core_text::convert($newpassword, 'utf-8', $this->config->ldapencoding);
1346
1347        switch ($this->config->passtype) {
1348            case 'md5':
1349                $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
1350                break;
1351            case 'sha1':
1352                $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
1353                break;
1354            case 'plaintext':
1355            default:
1356                break; // plaintext
1357        }
1358
1359        $ldapconnection = $this->ldap_connect();
1360
1361        $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
1362
1363        if (!$user_dn) {
1364            error_log($this->errorlogtag.get_string ('nodnforusername', 'auth_ldap', $user->username));
1365            return false;
1366        }
1367
1368        switch ($this->config->user_type) {
1369            case 'edir':
1370                // Change password
1371                $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
1372                if (!$result) {
1373                    error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap',
1374                                                               array('errno'=>ldap_errno($ldapconnection),
1375                                                                     'errstring'=>ldap_err2str(ldap_errno($ldapconnection)))));
1376                }
1377                // Update password expiration time, grace logins count
1378                $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval', 'loginGraceLimit');
1379                $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
1380                if ($sr) {
1381                    $entry = ldap_get_entries_moodle($ldapconnection, $sr);
1382                    $info = array_change_key_case($entry[0], CASE_LOWER);
1383                    $newattrs = array();
1384                    if (!empty($info[$this->config->expireattr][0])) {
1385                        // Set expiration time only if passwordExpirationInterval is defined
1386                        if (!empty($info['passwordexpirationinterval'][0])) {
1387                           $expirationtime = time() + $info['passwordexpirationinterval'][0];
1388                           $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime);
1389                           $newattrs['passwordExpirationTime'] = $ldapexpirationtime;
1390                        }
1391
1392                        // Set gracelogin count
1393                        if (!empty($info['logingracelimit'][0])) {
1394                           $newattrs['loginGraceRemaining']= $info['logingracelimit'][0];
1395                        }
1396
1397                        // Store attribute changes in LDAP
1398                        $result = ldap_modify($ldapconnection, $user_dn, $newattrs);
1399                        if (!$result) {
1400                            error_log($this->errorlogtag.get_string ('updatepasserrorexpiregrace', 'auth_ldap',
1401                                                                       array('errno'=>ldap_errno($ldapconnection),
1402                                                                             'errstring'=>ldap_err2str(ldap_errno($ldapconnection)))));
1403                        }
1404                    }
1405                }
1406                else {
1407                    error_log($this->errorlogtag.get_string ('updatepasserrorexpire', 'auth_ldap',
1408                                                             array('errno'=>ldap_errno($ldapconnection),
1409                                                                   'errstring'=>ldap_err2str(ldap_errno($ldapconnection)))));
1410                }
1411                break;
1412
1413            case 'ad':
1414                // Passwords in Active Directory must be encoded as Unicode
1415                // strings (UCS-2 Little Endian format) and surrounded with
1416                // double quotes. See http://support.microsoft.com/?kbid=269190
1417                if (!function_exists('mb_convert_encoding')) {
1418                    error_log($this->errorlogtag.get_string ('needmbstring', 'auth_ldap'));
1419                    return false;
1420                }
1421                $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding);
1422                $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword));
1423                if (!$result) {
1424                    error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap',
1425                                                             array('errno'=>ldap_errno($ldapconnection),
1426                                                                   'errstring'=>ldap_err2str(ldap_errno($ldapconnection)))));
1427                }
1428                break;
1429
1430            default:
1431                // Send LDAP the password in cleartext, it will md5 it itself
1432                $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
1433                if (!$result) {
1434                    error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap',
1435                                                             array('errno'=>ldap_errno($ldapconnection),
1436                                                                   'errstring'=>ldap_err2str(ldap_errno($ldapconnection)))));
1437                }
1438
1439        }
1440
1441        $this->ldap_close();
1442        return $result;
1443    }
1444
1445    /**
1446     * Take expirationtime and return it as unix timestamp in seconds
1447     *
1448     * Takes expiration timestamp as read from LDAP and returns it as unix timestamp in seconds
1449     * Depends on $this->config->user_type variable
1450     *
1451     * @param mixed time   Time stamp read from LDAP as it is.
1452     * @param string $ldapconnection Only needed for Active Directory.
1453     * @param string $user_dn User distinguished name for the user we are checking password expiration (only needed for Active Directory).
1454     * @return timestamp
1455     */
1456    function ldap_expirationtime2unix ($time, $ldapconnection, $user_dn) {
1457        $result = false;
1458        switch ($this->config->user_type) {
1459            case 'edir':
1460                $yr=substr($time, 0, 4);
1461                $mo=substr($time, 4, 2);
1462                $dt=substr($time, 6, 2);
1463                $hr=substr($time, 8, 2);
1464                $min=substr($time, 10, 2);
1465                $sec=substr($time, 12, 2);
1466                $result = mktime($hr, $min, $sec, $mo, $dt, $yr);
1467                break;
1468            case 'rfc2307':
1469            case 'rfc2307bis':
1470                $result = $time * DAYSECS; // The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date
1471                break;
1472            case 'ad':
1473                $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn);
1474                break;
1475            default:
1476                print_error('auth_ldap_usertypeundefined', 'auth_ldap');
1477        }
1478        return $result;
1479    }
1480
1481    /**
1482     * Takes unix timestamp and returns it formated for storing in LDAP
1483     *
1484     * @param integer unix time stamp
1485     */
1486    function ldap_unix2expirationtime($time) {
1487        $result = false;
1488        switch ($this->config->user_type) {
1489            case 'edir':
1490                $result=date('YmdHis', $time).'Z';
1491                break;
1492            case 'rfc2307':
1493            case 'rfc2307bis':
1494                $result = $time ; // Already in correct format
1495                break;
1496            default:
1497                print_error('auth_ldap_usertypeundefined2', 'auth_ldap');
1498        }
1499        return $result;
1500
1501    }
1502
1503    /**
1504     * Returns user attribute mappings between moodle and LDAP
1505     *
1506     * @return array
1507     */
1508
1509    function ldap_attributes () {
1510        $moodleattributes = array();
1511        // If we have custom fields then merge them with user fields.
1512        $customfields = $this->get_custom_user_profile_fields();
1513        if (!empty($customfields) && !empty($this->userfields)) {
1514            $userfields = array_merge($this->userfields, $customfields);
1515        } else {
1516            $userfields = $this->userfields;
1517        }
1518
1519        foreach ($userfields as $field) {
1520            if (!empty($this->config->{"field_map_$field"})) {
1521                $moodleattributes[$field] = core_text::strtolower(trim($this->config->{"field_map_$field"}));
1522                if (preg_match('/,/', $moodleattributes[$field])) {
1523                    $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ?
1524                }
1525            }
1526        }
1527        $moodleattributes['username'] = core_text::strtolower(trim($this->config->user_attribute));
1528        return $moodleattributes;
1529    }
1530
1531    /**
1532     * Returns all usernames from LDAP
1533     *
1534     * @param $filter An LDAP search filter to select desired users
1535     * @return array of LDAP user names converted to UTF-8
1536     */
1537    function ldap_get_userlist($filter='*') {
1538        $fresult = array();
1539
1540        $ldapconnection = $this->ldap_connect();
1541
1542        if ($filter == '*') {
1543           $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
1544        }
1545
1546        $contexts = explode(';', $this->config->contexts);
1547        if (!empty($this->config->create_context)) {
1548            array_push($contexts, $this->config->create_context);
1549        }
1550
1551        $ldap_cookie = '';
1552        $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version);
1553        foreach ($contexts as $context) {
1554            $context = trim($context);
1555            if (empty($context)) {
1556                continue;
1557            }
1558
1559            do {
1560                if ($ldap_pagedresults) {
1561                    ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
1562                }
1563                if ($this->config->search_sub) {
1564                    // Use ldap_search to find first user from subtree.
1565                    $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
1566                } else {
1567                    // Search only in this context.
1568                    $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
1569                }
1570                if(!$ldap_result) {
1571                    continue;
1572                }
1573                if ($ldap_pagedresults) {
1574                    ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
1575                }
1576                $users = ldap_get_entries_moodle($ldapconnection, $ldap_result);
1577                // Add found users to list.
1578                for ($i = 0; $i < count($users); $i++) {
1579                    $extuser = core_text::convert($users[$i][$this->config->user_attribute][0],
1580                                                $this->config->ldapencoding, 'utf-8');
1581                    array_push($fresult, $extuser);
1582                }
1583                unset($ldap_result); // Free mem.
1584            } while ($ldap_pagedresults && !empty($ldap_cookie));
1585        }
1586
1587        // If paged results were used, make sure the current connection is completely closed
1588        $this->ldap_close($ldap_pagedresults);
1589        return $fresult;
1590    }
1591
1592    /**
1593     * Indicates if password hashes should be stored in local moodle database.
1594     *
1595     * @return bool true means flag 'not_cached' stored instead of password hash
1596     */
1597    function prevent_local_passwords() {
1598        return !empty($this->config->preventpassindb);
1599    }
1600
1601    /**
1602     * Returns true if this authentication plugin is 'internal'.
1603     *
1604     * @return bool
1605     */
1606    function is_internal() {
1607        return false;
1608    }
1609
1610    /**
1611     * Returns true if this authentication plugin can change the user's
1612     * password.
1613     *
1614     * @return bool
1615     */
1616    function can_change_password() {
1617        return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl);
1618    }
1619
1620    /**
1621     * Returns the URL for changing the user's password, or empty if the default can
1622     * be used.
1623     *
1624     * @return moodle_url
1625     */
1626    function change_password_url() {
1627        if (empty($this->config->stdchangepassword)) {
1628            if (!empty($this->config->changepasswordurl)) {
1629                return new moodle_url($this->config->changepasswordurl);
1630            } else {
1631                return null;
1632            }
1633        } else {
1634            return null;
1635        }
1636    }
1637
1638    /**
1639     * Will get called before the login page is shownr. Ff NTLM SSO
1640     * is enabled, and the user is in the right network, we'll redirect
1641     * to the magic NTLM page for SSO...
1642     *
1643     */
1644    function loginpage_hook() {
1645        global $CFG, $SESSION;
1646
1647        // HTTPS is potentially required
1648        //httpsrequired(); - this must be used before setting the URL, it is already done on the login/index.php
1649
1650        if (($_SERVER['REQUEST_METHOD'] === 'GET'         // Only on initial GET of loginpage
1651             || ($_SERVER['REQUEST_METHOD'] === 'POST'
1652                 && (get_local_referer() != strip_querystring(qualified_me()))))
1653                                                          // Or when POSTed from another place
1654                                                          // See MDL-14071
1655            && !empty($this->config->ntlmsso_enabled)     // SSO enabled
1656            && !empty($this->config->ntlmsso_subnet)      // have a subnet to test for
1657            && empty($_GET['authldap_skipntlmsso'])       // haven't failed it yet
1658            && (isguestuser() || !isloggedin())           // guestuser or not-logged-in users
1659            && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) {
1660
1661            // First, let's remember where we were trying to get to before we got here
1662            if (empty($SESSION->wantsurl)) {
1663                $SESSION->wantsurl = null;
1664                $referer = get_local_referer(false);
1665                if ($referer &&
1666                        $referer != $CFG->wwwroot &&
1667                        $referer != $CFG->wwwroot . '/' &&
1668                        $referer != $CFG->httpswwwroot . '/login/' &&
1669                        $referer != $CFG->httpswwwroot . '/login/index.php') {
1670                    $SESSION->wantsurl = $referer;
1671                }
1672            }
1673
1674            // Now start the whole NTLM machinery.
1675            if($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESATTEMPT ||
1676                $this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) {
1677                if (core_useragent::is_ie()) {
1678                    $sesskey = sesskey();
1679                    redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey);
1680                } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) {
1681                    redirect($CFG->httpswwwroot.'/login/index.php?authldap_skipntlmsso=1');
1682                }
1683            }
1684            redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php');
1685        }
1686
1687        // No NTLM SSO, Use the normal login page instead.
1688
1689        // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login
1690        // page insists on redirecting us to that page after user validation. If
1691        // we clicked on the redirect link at the ntlmsso_finish.php page (instead
1692        // of waiting for the redirection to happen) then we have a 'Referer:' header
1693        // we don't want to use at all. As we can't get rid of it, just point
1694        // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there).
1695        if (empty($SESSION->wantsurl)
1696            && (get_local_referer() == $CFG->httpswwwroot.'/auth/ldap/ntlmsso_finish.php')) {
1697
1698            $SESSION->wantsurl = $CFG->wwwroot;
1699        }
1700    }
1701
1702    /**
1703     * To be called from a page running under NTLM's
1704     * "Integrated Windows Authentication".
1705     *
1706     * If successful, it will set a special "cookie" (not an HTTP cookie!)
1707     * in cache_flags under the $this->pluginconfig/ntlmsess "plugin" and return true.
1708     * The "cookie" will be picked up by ntlmsso_finish() to complete the
1709     * process.
1710     *
1711     * On failure it will return false for the caller to display an appropriate
1712     * error message (probably saying that Integrated Windows Auth isn't enabled!)
1713     *
1714     * NOTE that this code will execute under the OS user credentials,
1715     * so we MUST avoid dealing with files -- such as session files.
1716     * (The caller should define('NO_MOODLE_COOKIES', true) before including config.php)
1717     *
1718     */
1719    function ntlmsso_magic($sesskey) {
1720        if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) {
1721
1722            // HTTP __headers__ seem to be sent in ISO-8859-1 encoding
1723            // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and
1724            // my local tests), so we need to convert the REMOTE_USER value
1725            // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8
1726            $username = core_text::convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8');
1727
1728            switch ($this->config->ntlmsso_type) {
1729                case 'ntlm':
1730                    // The format is now configurable, so try to extract the username
1731                    $username = $this->get_ntlm_remote_user($username);
1732                    if (empty($username)) {
1733                        return false;
1734                    }
1735                    break;
1736                case 'kerberos':
1737                    // Format is username@DOMAIN
1738                    $username = substr($username, 0, strpos($username, '@'));
1739                    break;
1740                default:
1741                    error_log($this->errorlogtag.get_string ('ntlmsso_unknowntype', 'auth_ldap'));
1742                    return false; // Should never happen!
1743            }
1744
1745            $username = core_text::strtolower($username); // Compatibility hack
1746            set_cache_flag($this->pluginconfig.'/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT);
1747            return true;
1748        }
1749        return false;
1750    }
1751
1752    /**
1753     * Find the session set by ntlmsso_magic(), validate it and
1754     * call authenticate_user_login() to authenticate the user through
1755     * the auth machinery.
1756     *
1757     * It is complemented by a similar check in user_login().
1758     *
1759     * If it succeeds, it never returns.
1760     *
1761     */
1762    function ntlmsso_finish() {
1763        global $CFG, $USER, $SESSION;
1764
1765        $key = sesskey();
1766        $cf = get_cache_flags($this->pluginconfig.'/ntlmsess');
1767        if (!isset($cf[$key]) || $cf[$key] === '') {
1768            return false;
1769        }
1770        $username   = $cf[$key];
1771
1772        // Here we want to trigger the whole authentication machinery
1773        // to make sure no step is bypassed...
1774        $user = authenticate_user_login($username, $key);
1775        if ($user) {
1776            complete_user_login($user);
1777
1778            // Cleanup the key to prevent reuse...
1779            // and to allow re-logins with normal credentials
1780            unset_cache_flag($this->pluginconfig.'/ntlmsess', $key);
1781
1782            // Redirection
1783            if (user_not_fully_set_up($USER)) {
1784                $urltogo = $CFG->wwwroot.'/user/edit.php';
1785                // We don't delete $SESSION->wantsurl yet, so we get there later
1786            } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) {
1787                $urltogo = $SESSION->wantsurl;    // Because it's an address in this site
1788                unset($SESSION->wantsurl);
1789            } else {
1790                // No wantsurl stored or external - go to homepage
1791                $urltogo = $CFG->wwwroot.'/';
1792                unset($SESSION->wantsurl);
1793            }
1794            // We do not want to redirect if we are in a PHPUnit test.
1795            if (!PHPUNIT_TEST) {
1796                redirect($urltogo);
1797            }
1798        }
1799        // Should never reach here.
1800        return false;
1801    }
1802
1803    /**
1804     * Sync roles for this user
1805     *
1806     * @param $user object user object (without system magic quotes)
1807     */
1808    function sync_roles($user) {
1809        $iscreator = $this->iscreator($user->username);
1810        if ($iscreator === null) {
1811            return; // Nothing to sync - creators not configured
1812        }
1813
1814        if ($roles = get_archetype_roles('coursecreator')) {
1815            $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
1816            $systemcontext = context_system::instance();
1817
1818            if ($iscreator) { // Following calls will not create duplicates
1819                role_assign($creatorrole->id, $user->id, $systemcontext->id, $this->roleauth);
1820            } else {
1821                // Unassign only if previously assigned by this plugin!
1822                role_unassign($creatorrole->id, $user->id, $systemcontext->id, $this->roleauth);
1823            }
1824        }
1825    }
1826
1827    /**
1828     * Prints a form for configuring this authentication plugin.
1829     *
1830     * This function is called from admin/auth.php, and outputs a full page with
1831     * a form for configuring this plugin.
1832     *
1833     * @param array $page An object containing all the data for this page.
1834     */
1835    function config_form($config, $err, $user_fields) {
1836        global $CFG, $OUTPUT;
1837
1838        if (!function_exists('ldap_connect')) { // Is php-ldap really there?
1839            echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'));
1840            return;
1841        }
1842
1843        include($CFG->dirroot.'/auth/ldap/config.html');
1844    }
1845
1846    /**
1847     * Processes and stores configuration data for this authentication plugin.
1848     */
1849    function process_config($config) {
1850        // Set to defaults if undefined
1851        if (!isset($config->host_url)) {
1852             $config->host_url = '';
1853        }
1854        if (!isset($config->start_tls)) {
1855             $config->start_tls = false;
1856        }
1857        if (empty($config->ldapencoding)) {
1858         $config->ldapencoding = 'utf-8';
1859        }
1860        if (!isset($config->pagesize)) {
1861            $config->pagesize = LDAP_DEFAULT_PAGESIZE;
1862        }
1863        if (!isset($config->contexts)) {
1864             $config->contexts = '';
1865        }
1866        if (!isset($config->user_type)) {
1867             $config->user_type = 'default';
1868        }
1869        if (!isset($config->user_attribute)) {
1870             $config->user_attribute = '';
1871        }
1872        if (!isset($config->search_sub)) {
1873             $config->search_sub = '';
1874        }
1875        if (!isset($config->opt_deref)) {
1876             $config->opt_deref = LDAP_DEREF_NEVER;
1877        }
1878        if (!isset($config->preventpassindb)) {
1879             $config->preventpassindb = 0;
1880        }
1881        if (!isset($config->bind_dn)) {
1882            $config->bind_dn = '';
1883        }
1884        if (!isset($config->bind_pw)) {
1885            $config->bind_pw = '';
1886        }
1887        if (!isset($config->ldap_version)) {
1888            $config->ldap_version = '3';
1889        }
1890        if (!isset($config->objectclass)) {
1891            $config->objectclass = '';
1892        }
1893        if (!isset($config->memberattribute)) {
1894            $config->memberattribute = '';
1895        }
1896        if (!isset($config->memberattribute_isdn)) {
1897            $config->memberattribute_isdn = '';
1898        }
1899        if (!isset($config->creators)) {
1900            $config->creators = '';
1901        }
1902        if (!isset($config->create_context)) {
1903            $config->create_context = '';
1904        }
1905        if (!isset($config->expiration)) {
1906            $config->expiration = '';
1907        }
1908        if (!isset($config->expiration_warning)) {
1909            $config->expiration_warning = '10';
1910        }
1911        if (!isset($config->expireattr)) {
1912            $config->expireattr = '';
1913        }
1914        if (!isset($config->gracelogins)) {
1915            $config->gracelogins = '';
1916        }
1917        if (!isset($config->graceattr)) {
1918            $config->graceattr = '';
1919        }
1920        if (!isset($config->auth_user_create)) {
1921            $config->auth_user_create = '';
1922        }
1923        if (!isset($config->forcechangepassword)) {
1924            $config->forcechangepassword = 0;
1925        }
1926        if (!isset($config->stdchangepassword)) {
1927            $config->stdchangepassword = 0;
1928        }
1929        if (!isset($config->passtype)) {
1930            $config->passtype = 'plaintext';
1931        }
1932        if (!isset($config->changepasswordurl)) {
1933            $config->changepasswordurl = '';
1934        }
1935        if (!isset($config->removeuser)) {
1936            $config->removeuser = AUTH_REMOVEUSER_KEEP;
1937        }
1938        if (!isset($config->ntlmsso_enabled)) {
1939            $config->ntlmsso_enabled = 0;
1940        }
1941        if (!isset($config->ntlmsso_subnet)) {
1942            $config->ntlmsso_subnet = '';
1943        }
1944        if (!isset($config->ntlmsso_ie_fastpath)) {
1945            $config->ntlmsso_ie_fastpath = 0;
1946        }
1947        if (!isset($config->ntlmsso_type)) {
1948            $config->ntlmsso_type = 'ntlm';
1949        }
1950        if (!isset($config->ntlmsso_remoteuserformat)) {
1951            $config->ntlmsso_remoteuserformat = '';
1952        }
1953
1954        // Try to remove duplicates before storing the contexts (to avoid problems in sync_users()).
1955        $config->contexts = explode(';', $config->contexts);
1956        $config->contexts = array_map(create_function('$x', 'return core_text::strtolower(trim($x));'),
1957                                      $config->contexts);
1958        $config->contexts = implode(';', array_unique($config->contexts));
1959
1960        // Save settings
1961        set_config('host_url', trim($config->host_url), $this->pluginconfig);
1962        set_config('start_tls', $config->start_tls, $this->pluginconfig);
1963        set_config('ldapencoding', trim($config->ldapencoding), $this->pluginconfig);
1964        set_config('pagesize', (int)trim($config->pagesize), $this->pluginconfig);
1965        set_config('contexts', $config->contexts, $this->pluginconfig);
1966        set_config('user_type', core_text::strtolower(trim($config->user_type)), $this->pluginconfig);
1967        set_config('user_attribute', core_text::strtolower(trim($config->user_attribute)), $this->pluginconfig);
1968        set_config('search_sub', $config->search_sub, $this->pluginconfig);
1969        set_config('opt_deref', $config->opt_deref, $this->pluginconfig);
1970        set_config('preventpassindb', $config->preventpassindb, $this->pluginconfig);
1971        set_config('bind_dn', trim($config->bind_dn), $this->pluginconfig);
1972        set_config('bind_pw', $config->bind_pw, $this->pluginconfig);
1973        set_config('ldap_version', $config->ldap_version, $this->pluginconfig);
1974        set_config('objectclass', trim($config->objectclass), $this->pluginconfig);
1975        set_config('memberattribute', core_text::strtolower(trim($config->memberattribute)), $this->pluginconfig);
1976        set_config('memberattribute_isdn', $config->memberattribute_isdn, $this->pluginconfig);
1977        set_config('creators', trim($config->creators), $this->pluginconfig);
1978        set_config('create_context', trim($config->create_context), $this->pluginconfig);
1979        set_config('expiration', $config->expiration, $this->pluginconfig);
1980        set_config('expiration_warning', trim($config->expiration_warning), $this->pluginconfig);
1981        set_config('expireattr', core_text::strtolower(trim($config->expireattr)), $this->pluginconfig);
1982        set_config('gracelogins', $config->gracelogins, $this->pluginconfig);
1983        set_config('graceattr', core_text::strtolower(trim($config->graceattr)), $this->pluginconfig);
1984        set_config('auth_user_create', $config->auth_user_create, $this->pluginconfig);
1985        set_config('forcechangepassword', $config->forcechangepassword, $this->pluginconfig);
1986        set_config('stdchangepassword', $config->stdchangepassword, $this->pluginconfig);
1987        set_config('passtype', $config->passtype, $this->pluginconfig);
1988        set_config('changepasswordurl', trim($config->changepasswordurl), $this->pluginconfig);
1989        set_config('removeuser', $config->removeuser, $this->pluginconfig);
1990        set_config('ntlmsso_enabled', (int)$config->ntlmsso_enabled, $this->pluginconfig);
1991        set_config('ntlmsso_subnet', trim($config->ntlmsso_subnet), $this->pluginconfig);
1992        set_config('ntlmsso_ie_fastpath', (int)$config->ntlmsso_ie_fastpath, $this->pluginconfig);
1993        set_config('ntlmsso_type', $config->ntlmsso_type, 'auth/ldap');
1994        set_config('ntlmsso_remoteuserformat', trim($config->ntlmsso_remoteuserformat), 'auth/ldap');
1995
1996        return true;
1997    }
1998
1999    /**
2000     * Get password expiration time for a given user from Active Directory
2001     *
2002     * @param string $pwdlastset The time last time we changed the password.
2003     * @param resource $lcapconn The open LDAP connection.
2004     * @param string $user_dn The distinguished name of the user we are checking.
2005     *
2006     * @return string $unixtime
2007     */
2008    function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){
2009        global $CFG;
2010
2011        if (!function_exists('bcsub')) {
2012            error_log($this->errorlogtag.get_string ('needbcmath', 'auth_ldap'));
2013            return 0;
2014        }
2015
2016        // If UF_DONT_EXPIRE_PASSWD flag is set in user's
2017        // userAccountControl attribute, the password doesn't expire.
2018        $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)',
2019                        array('userAccountControl'));
2020        if (!$sr) {
2021            error_log($this->errorlogtag.get_string ('useracctctrlerror', 'auth_ldap', $user_dn));
2022            // Don't expire password, as we are not sure if it has to be
2023            // expired or not.
2024            return 0;
2025        }
2026
2027        $entry = ldap_get_entries_moodle($ldapconn, $sr);
2028        $info = array_change_key_case($entry[0], CASE_LOWER);
2029        $useraccountcontrol = $info['useraccountcontrol'][0];
2030        if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) {
2031            // Password doesn't expire.
2032            return 0;
2033        }
2034
2035        // If pwdLastSet is zero, the user must change his/her password now
2036        // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already
2037        // tested this above)
2038        if ($pwdlastset === '0') {
2039            // Password has expired
2040            return -1;
2041        }
2042
2043        // ----------------------------------------------------------------
2044        // Password expiration time in Active Directory is the composition of
2045        // two values:
2046        //
2047        //   - User's pwdLastSet attribute, that stores the last time
2048        //     the password was changed.
2049        //
2050        //   - Domain's maxPwdAge attribute, that sets how long
2051        //     passwords last in this domain.
2052        //
2053        // We already have the first value (passed in as a parameter). We
2054        // need to get the second one. As we don't know the domain DN, we
2055        // have to query rootDSE's defaultNamingContext attribute to get
2056        // it. Then we have to query that DN's maxPwdAge attribute to get
2057        // the real value.
2058        //
2059        // Once we have both values, we just need to combine them. But MS
2060        // chose to use a different base and unit for time measurements.
2061        // So we need to convert the values to Unix timestamps (see
2062        // details below).
2063        // ----------------------------------------------------------------
2064
2065        $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)',
2066                        array('defaultNamingContext'));
2067        if (!$sr) {
2068            error_log($this->errorlogtag.get_string ('rootdseerror', 'auth_ldap'));
2069            return 0;
2070        }
2071
2072        $entry = ldap_get_entries_moodle($ldapconn, $sr);
2073        $info = array_change_key_case($entry[0], CASE_LOWER);
2074        $domaindn = $info['defaultnamingcontext'][0];
2075
2076        $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)',
2077                         array('maxPwdAge'));
2078        $entry = ldap_get_entries_moodle($ldapconn, $sr);
2079        $info = array_change_key_case($entry[0], CASE_LOWER);
2080        $maxpwdage = $info['maxpwdage'][0];
2081
2082        // ----------------------------------------------------------------
2083        // MSDN says that "pwdLastSet contains the number of 100 nanosecond
2084        // intervals since January 1, 1601 (UTC), stored in a 64 bit integer".
2085        //
2086        // According to Perl's Date::Manip, the number of seconds between
2087        // this date and Unix epoch is 11644473600. So we have to
2088        // substract this value to calculate a Unix time, once we have
2089        // scaled pwdLastSet to seconds. This is the script used to
2090        // calculate the value shown above:
2091        //
2092        //    #!/usr/bin/perl -w
2093        //
2094        //    use Date::Manip;
2095        //
2096        //    $date1 = ParseDate ("160101010000 UTC");
2097        //    $date2 = ParseDate ("197001010000 UTC");
2098        //    $delta = DateCalc($date1, $date2, \$err);
2099        //    $secs = Delta_Format($delta, 0, "%st");
2100        //    print "$secs \n";
2101        //
2102        // MSDN also says that "maxPwdAge is stored as a large integer that
2103        // represents the number of 100 nanosecond intervals from the time
2104        // the password was set before the password expires." We also need
2105        // to scale this to seconds. Bear in mind that this value is stored
2106        // as a _negative_ quantity (at least in my AD domain).
2107        //
2108        // As a last remark, if the low 32 bits of maxPwdAge are equal to 0,
2109        // the maximum password age in the domain is set to 0, which means
2110        // passwords do not expire (see
2111        // http://msdn2.microsoft.com/en-us/library/ms974598.aspx)
2112        //
2113        // As the quantities involved are too big for PHP integers, we
2114        // need to use BCMath functions to work with arbitrary precision
2115        // numbers.
2116        // ----------------------------------------------------------------
2117
2118        // If the low order 32 bits are 0, then passwords do not expire in
2119        // the domain. Just do '$maxpwdage mod 2^32' and check the result
2120        // (2^32 = 4294967296)
2121        if (bcmod ($maxpwdage, 4294967296) === '0') {
2122            return 0;
2123        }
2124
2125        // Add up pwdLastSet and maxPwdAge to get password expiration
2126        // time, in MS time units. Remember maxPwdAge is stored as a
2127        // _negative_ quantity, so we need to substract it in fact.
2128        $pwdexpire = bcsub ($pwdlastset, $maxpwdage);
2129
2130        // Scale the result to convert it to Unix time units and return
2131        // that value.
2132        return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600');
2133    }
2134
2135    /**
2136     * Connect to the LDAP server, using the plugin configured
2137     * settings. It's actually a wrapper around ldap_connect_moodle()
2138     *
2139     * @return resource A valid LDAP connection (or dies if it can't connect)
2140     */
2141    function ldap_connect() {
2142        // Cache ldap connections. They are expensive to set up
2143        // and can drain the TCP/IP ressources on the server if we
2144        // are syncing a lot of users (as we try to open a new connection
2145        // to get the user details). This is the least invasive way
2146        // to reuse existing connections without greater code surgery.
2147        if(!empty($this->ldapconnection)) {
2148            $this->ldapconns++;
2149            return $this->ldapconnection;
2150        }
2151
2152        if($ldapconnection = ldap_connect_moodle($this->config->host_url, $this->config->ldap_version,
2153                                                 $this->config->user_type, $this->config->bind_dn,
2154                                                 $this->config->bind_pw, $this->config->opt_deref,
2155                                                 $debuginfo, $this->config->start_tls)) {
2156            $this->ldapconns = 1;
2157            $this->ldapconnection = $ldapconnection;
2158            return $ldapconnection;
2159        }
2160
2161        print_error('auth_ldap_noconnect_all', 'auth_ldap', '', $debuginfo);
2162    }
2163
2164    /**
2165     * Disconnects from a LDAP server
2166     *
2167     * @param force boolean Forces closing the real connection to the LDAP server, ignoring any
2168     *                      cached connections. This is needed when we've used paged results
2169     *                      and want to use normal results again.
2170     */
2171    function ldap_close($force=false) {
2172        $this->ldapconns--;
2173        if (($this->ldapconns == 0) || ($force)) {
2174            $this->ldapconns = 0;
2175            @ldap_close($this->ldapconnection);
2176            unset($this->ldapconnection);
2177        }
2178    }
2179
2180    /**
2181     * Search specified contexts for username and return the user dn
2182     * like: cn=username,ou=suborg,o=org. It's actually a wrapper
2183     * around ldap_find_userdn().
2184     *
2185     * @param resource $ldapconnection a valid LDAP connection
2186     * @param string $extusername the username to search (in external LDAP encoding, no db slashes)
2187     * @return mixed the user dn (external LDAP encoding) or false
2188     */
2189    function ldap_find_userdn($ldapconnection, $extusername) {
2190        $ldap_contexts = explode(';', $this->config->contexts);
2191        if (!empty($this->config->create_context)) {
2192            array_push($ldap_contexts, $this->config->create_context);
2193        }
2194
2195        return ldap_find_userdn($ldapconnection, $extusername, $ldap_contexts, $this->config->objectclass,
2196                                $this->config->user_attribute, $this->config->search_sub);
2197    }
2198
2199
2200    /**
2201     * A chance to validate form data, and last chance to do stuff
2202     * before it is inserted in config_plugin
2203     *
2204     * @param object object with submitted configuration settings (without system magic quotes)
2205     * @param array $err array of error messages (passed by reference)
2206     */
2207    function validate_form($form, &$err) {
2208        if ($form->ntlmsso_type == 'ntlm') {
2209            $format = trim($form->ntlmsso_remoteuserformat);
2210            if (!empty($format) && !preg_match('/%username%/i', $format)) {
2211                $err['ntlmsso_remoteuserformat'] = get_string('auth_ntlmsso_missing_username', 'auth_ldap');
2212            }
2213        }
2214    }
2215
2216
2217    /**
2218     * When using NTLM SSO, the format of the remote username we get in
2219     * $_SERVER['REMOTE_USER'] may vary, depending on where from and how the web
2220     * server gets the data. So we let the admin configure the format using two
2221     * place holders (%domain% and %username%). This function tries to extract
2222     * the username (stripping the domain part and any separators if they are
2223     * present) from the value present in $_SERVER['REMOTE_USER'], using the
2224     * configured format.
2225     *
2226     * @param string $remoteuser The value from $_SERVER['REMOTE_USER'] (converted to UTF-8)
2227     *
2228     * @return string The remote username (without domain part or
2229     *                separators). Empty string if we can't extract the username.
2230     */
2231    protected function get_ntlm_remote_user($remoteuser) {
2232        if (empty($this->config->ntlmsso_remoteuserformat)) {
2233            $format = AUTH_NTLM_DEFAULT_FORMAT;
2234        } else {
2235            $format = $this->config->ntlmsso_remoteuserformat;
2236        }
2237
2238        $format = preg_quote($format);
2239        $formatregex = preg_replace(array('#%domain%#', '#%username%#'),
2240                                    array('('.AUTH_NTLM_VALID_DOMAINNAME.')', '('.AUTH_NTLM_VALID_USERNAME.')'),
2241                                    $format);
2242        if (preg_match('#^'.$formatregex.'$#', $remoteuser, $matches)) {
2243            $user = end($matches);
2244            return $user;
2245        }
2246
2247        /* We are unable to extract the username with the configured format. Probably
2248         * the format specified is wrong, so log a warning for the admin and return
2249         * an empty username.
2250         */
2251        error_log($this->errorlogtag.get_string ('auth_ntlmsso_maybeinvalidformat', 'auth_ldap'));
2252        return '';
2253    }
2254
2255    /**
2256     * Check if the diagnostic message for the LDAP login error tells us that the
2257     * login is denied because the user password has expired or the password needs
2258     * to be changed on first login (using interactive SMB/Windows logins, not
2259     * LDAP logins).
2260     *
2261     * @param string the diagnostic message for the LDAP login error
2262     * @return bool true if the password has expired or the password must be changed on first login
2263     */
2264    protected function ldap_ad_pwdexpired_from_diagmsg($diagmsg) {
2265        // The format of the diagnostic message is (actual examples from W2003 and W2008):
2266        // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 52e, vece"  (W2003)
2267        // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 773, vece"  (W2003)
2268        // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 52e, v1771" (W2008)
2269        // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 773, v1771" (W2008)
2270        // We are interested in the 'data nnn' part.
2271        //   if nnn == 773 then user must change password on first login
2272        //   if nnn == 532 then user password has expired
2273        $diagmsg = explode(',', $diagmsg);
2274        if (preg_match('/data (773|532)/i', trim($diagmsg[2]))) {
2275            return true;
2276        }
2277        return false;
2278    }
2279
2280} // End of the class
Note: See TracBrowser for help on using the repository browser.