auth.php 84 KB
Newer Older
1
2
3
4
<?php

/**
 * @author Martin Dougiamas
5
 * @author I�aki Arenaza
6
7
8
9
10
11
12
13
14
15
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package moodle multiauth
 *
 * Authentication Plugin: LDAP Authentication
 *
 * Authentication using LDAP (Lightweight Directory Access Protocol).
 *
 * 2006-08-28  File created.
 */

skodak's avatar
skodak committed
16
17
if (!defined('MOODLE_INTERNAL')) {
    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
18
19
}

20
21
22
23
24
25
26
// See http://support.microsoft.com/kb/305144 to interprete these values.
if (!defined('AUTH_AD_ACCOUNTDISABLE')) {
    define('AUTH_AD_ACCOUNTDISABLE', 0x0002);
}
if (!defined('AUTH_AD_NORMAL_ACCOUNT')) {
    define('AUTH_AD_NORMAL_ACCOUNT', 0x0200);
}
27
28
29
30
if (!defined('AUTH_NTLMTIMEOUT')) {  // timewindow for the NTLM SSO process, in secs...
    define('AUTH_NTLMTIMEOUT', 10);
}

31
32
33
34
35
36
37
38
39
40
41
42
// UF_DONT_EXPIRE_PASSWD value taken from MSDN directly
if (!defined('UF_DONT_EXPIRE_PASSWD')) {
    define ('UF_DONT_EXPIRE_PASSWD', 0x00010000);
}

// The Posix uid and gid of the 'nobody' account and 'nogroup' group.
if (!defined('AUTH_UID_NOBODY')) {
    define('AUTH_UID_NOBODY', -2);
}
if (!defined('AUTH_GID_NOGROUP')) {
    define('AUTH_GID_NOGROUP', -2);
}
43

44
require_once($CFG->libdir.'/authlib.php');
45
require_once($CFG->libdir.'/ldaplib.php');
46

47
48
49
/**
 * LDAP authentication plugin.
 */
50
class auth_plugin_ldap extends auth_plugin_base {
51
52

    /**
53
     * Init plugin config from database settings depending on the plugin auth type.
54
     */
55
56
57
    function init_plugin($authtype) {
        $this->pluginconfig = 'auth/'.$authtype;
        $this->config = get_config($this->pluginconfig);
skodak's avatar
skodak committed
58
59
60
61
62
63
64
        if (empty($this->config->ldapencoding)) {
            $this->config->ldapencoding = 'utf-8';
        }
        if (empty($this->config->user_type)) {
            $this->config->user_type = 'default';
        }

65
66
67
68
69
        $ldap_usertypes = ldap_supported_usertypes();
        $this->config->user_type_name = $ldap_usertypes[$this->config->user_type];
        unset($ldap_usertypes);

        $default = ldap_getdefaults();
skodak's avatar
skodak committed
70

71
        // Use defaults if values not given
skodak's avatar
skodak committed
72
73
74
75
76
77
        foreach ($default as $key => $value) {
            // watch out - 0, false are correct values too
            if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
                $this->config->{$key} = $value[$this->config->user_type];
            }
        }
78
79
80
81

        // Hack prefix to objectclass
        if (empty($this->config->objectclass)) {
            // Can't send empty filter
82
            $this->config->objectclass = '(objectClass=*)';
83
84
85
86
        } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
            // Value is 'objectClass=some-string-here', so just add ()
            // around the value (filter _must_ have them).
            $this->config->objectclass = '('.$this->config->objectclass.')';
87
        } else if (strpos($this->config->objectclass, '(') !== 0) {
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
            // Value is 'some-string-not-starting-with-left-parentheses',
            // which is assumed to be the objectClass matching value.
            // So build a valid filter with it.
            $this->config->objectclass = '(objectClass='.$this->config->objectclass.')';
        } else {
            // There is an additional possible value
            // '(some-string-here)', that can be used to specify any
            // valid filter string, to select subsets of users based
            // on any criteria. For example, we could select the users
            // whose objectClass is 'user' and have the
            // 'enabledMoodleUser' attribute, with something like:
            //
            //   (&(objectClass=user)(enabledMoodleUser=1))
            //
            // In this particular case we don't need to do anything,
            // so leave $this->config->objectclass as is.
skodak's avatar
skodak committed
104
        }
105
    }
skodak's avatar
skodak committed
106

107
108
109
110
111
112
113
114
    /**
     * Constructor with initialisation.
     */
    function auth_plugin_ldap() {
        $this->authtype = 'ldap';
        $this->roleauth = 'auth_ldap';
        $this->errorlogtag = '[AUTH LDAP] ';
        $this->init_plugin($this->authtype);
115
116
117
118
119
120
    }

    /**
     * Returns true if the username and password work and false if they are
     * wrong or don't exist.
     *
121
122
     * @param string $username The username (without system magic quotes)
     * @param string $password The password (without system magic quotes)
skodak's avatar
skodak committed
123
124
     *
     * @return bool Authentication success or failure.
125
126
     */
    function user_login($username, $password) {
donal72's avatar
donal72 committed
127
        if (! function_exists('ldap_bind')) {
128
            print_error('auth_ldapnotinstalled', 'auth_ldap');
donal72's avatar
donal72 committed
129
130
            return false;
        }
131
132
133
134

        if (!$username or !$password) {    // Don't allow blank usernames or passwords
            return false;
        }
skodak's avatar
skodak committed
135

136
        $textlib = textlib_get_instance();
skodak's avatar
skodak committed
137
138
        $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
        $extpassword = $textlib->convert($password, 'utf-8', $this->config->ldapencoding);
139

140
        // Before we connect to LDAP, check if this is an AD SSO login
141
        // if we succeed in this block, we'll return success early.
142
        //
143
144
        $key = sesskey();
        if (!empty($this->config->ntlmsso_enabled) && $key === $password) {
145
            $cf = get_cache_flags($this->pluginconfig.'/ntlmsess');
146
147
148
149
150
151
152
153
154
155
156
157
158
159
            // We only get the cache flag if we retrieve it before
            // it expires (AUTH_NTLMTIMEOUT seconds).
            if (!isset($cf[$key]) || $cf[$key] === '') {
                return false;
            }

            $sessusername = $cf[$key];
            if ($username === $sessusername) {
                unset($sessusername);
                unset($cf);

                // Check that the user is inside one of the configured LDAP contexts
                $validuser = false;
                $ldapconnection = $this->ldap_connect();
160
161
162
163
                // if the user is not inside the configured contexts,
                // ldap_find_userdn returns false.
                if ($this->ldap_find_userdn($ldapconnection, $extusername)) {
                    $validuser = true;
164
                }
165
                $this->ldap_close();
166
167
168

                // Shortcut here - SSO confirmed
                return $validuser;
169
            }
170
        } // End SSO processing
171
        unset($key);
172

173
        $ldapconnection = $this->ldap_connect();
174
        $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
skodak's avatar
skodak committed
175

176
177
        // If ldap_user_dn is empty, user does not exist
        if (!$ldap_user_dn) {
178
            $this->ldap_close();
179
            return false;
180
        }
181
182
183
184
185
186

        // Try to bind with current username and password
        $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword);
        $this->ldap_close();
        if ($ldap_login) {
            return true;
187
188
189
190
191
        }
        return false;
    }

    /**
192
     * Reads user information from ldap and returns it in array()
193
194
195
196
     *
     * Function should return all information available. If you are saving
     * this information to moodle user-table you should honor syncronization flags
     *
skodak's avatar
skodak committed
197
     * @param string $username username
skodak's avatar
skodak committed
198
199
     *
     * @return mixed array with no magic quotes or false on error
200
201
     */
    function get_userinfo($username) {
skodak's avatar
skodak committed
202
        $textlib = textlib_get_instance();
skodak's avatar
skodak committed
203
        $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
skodak's avatar
skodak committed
204

205
        $ldapconnection = $this->ldap_connect();
206
207
208
        if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) {
            return false;
        }
skodak's avatar
skodak committed
209

210
        $search_attribs = array();
211
212
        $attrmap = $this->ldap_attributes();
        foreach ($attrmap as $key => $values) {
213
214
215
216
217
218
            if (!is_array($values)) {
                $values = array($values);
            }
            foreach ($values as $value) {
                if (!in_array($value, $search_attribs)) {
                    array_push($search_attribs, $value);
skodak's avatar
skodak committed
219
                }
220
221
222
            }
        }

223
        if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) {
skodak's avatar
skodak committed
224
225
            return false; // error!
        }
226
227

        $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result);
skodak's avatar
skodak committed
228
229
230
231
        if (empty($user_entry)) {
            return false; // entry not found
        }

232
233
        $result = array();
        foreach ($attrmap as $key => $values) {
skodak's avatar
skodak committed
234
235
236
237
238
            if (!is_array($values)) {
                $values = array($values);
            }
            $ldapval = NULL;
            foreach ($values as $value) {
239
240
                $entry = array_change_key_case($user_entry[0], CASE_LOWER);
                if (($value == 'dn') || ($value == 'distinguishedname')) {
241
                    $result[$key] = $user_dn;
242
                    continue;
243
                }
244
                if (!array_key_exists($value, $entry)) {
skodak's avatar
skodak committed
245
                    continue; // wrong data mapping!
246
                }
247
248
                if (is_array($entry[$value])) {
                    $newval = $textlib->convert($entry[$value][0], $this->config->ldapencoding, 'utf-8');
skodak's avatar
skodak committed
249
                } else {
250
                    $newval = $textlib->convert($entry[$value], $this->config->ldapencoding, 'utf-8');
251
                }
skodak's avatar
skodak committed
252
253
                if (!empty($newval)) { // favour ldap entries that are set
                    $ldapval = $newval;
254
255
                }
            }
skodak's avatar
skodak committed
256
257
258
            if (!is_null($ldapval)) {
                $result[$key] = $ldapval;
            }
259
260
        }

261
        $this->ldap_close();
262
263
264
265
        return $result;
    }

    /**
266
     * Reads user information from ldap and returns it in an object
267
     *
skodak's avatar
skodak committed
268
269
     * @param string $username username (with system magic quotes)
     * @return mixed object or false on error
270
271
     */
    function get_userinfo_asobj($username) {
skodak's avatar
skodak committed
272
273
274
275
276
        $user_array = $this->get_userinfo($username);
        if ($user_array == false) {
            return false; //error or not found
        }
        $user_array = truncate_userinfo($user_array);
277
        $user = new stdClass();
278
279
280
281
282
283
284
        foreach ($user_array as $key=>$value) {
            $user->{$key} = $value;
        }
        return $user;
    }

    /**
285
     * Returns all usernames from LDAP
286
     *
287
     * get_userlist returns all usernames from LDAP
288
     *
skodak's avatar
skodak committed
289
     * @return array
290
291
292
293
294
295
     */
    function get_userlist() {
        return $this->ldap_get_userlist("({$this->config->user_attribute}=*)");
    }

    /**
296
     * Checks if user exists on LDAP
skodak's avatar
skodak committed
297
     *
skodak's avatar
skodak committed
298
     * @param string $username
299
300
     */
    function user_exists($username) {
skodak's avatar
skodak committed
301
        $textlib = textlib_get_instance();
skodak's avatar
skodak committed
302
        $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
skodak's avatar
skodak committed
303

304
305
        // Returns true if given username exists on ldap
        $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')');
skodak's avatar
skodak committed
306
        return count($users);
307
308
309
    }

    /**
310
     * Creates a new user on LDAP.
311
     * By using information in userobject
312
     * Use user_exists to prevent duplicate usernames
313
     *
skodak's avatar
skodak committed
314
315
     * @param mixed $userobject  Moodle userobject
     * @param mixed $plainpass   Plaintext password
316
317
     */
    function user_create($userobject, $plainpass) {
skodak's avatar
skodak committed
318
        $textlib = textlib_get_instance();
skodak's avatar
skodak committed
319
320
        $extusername = $textlib->convert($userobject->username, 'utf-8', $this->config->ldapencoding);
        $extpassword = $textlib->convert($plainpass, 'utf-8', $this->config->ldapencoding);
skodak's avatar
skodak committed
321

322
323
324
325
326
327
328
329
330
331
332
333
        switch ($this->config->passtype) {
            case 'md5':
                $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
                break;
            case 'sha1':
                $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
                break;
            case 'plaintext':
            default:
                break; // plaintext
        }

334
335
        $ldapconnection = $this->ldap_connect();
        $attrmap = $this->ldap_attributes();
skodak's avatar
skodak committed
336

337
        $newuser = array();
skodak's avatar
skodak committed
338

339
340
341
342
343
344
        foreach ($attrmap as $key => $values) {
            if (!is_array($values)) {
                $values = array($values);
            }
            foreach ($values as $value) {
                if (!empty($userobject->$key) ) {
skodak's avatar
skodak committed
345
                    $newuser[$value] = $textlib->convert($userobject->$key, 'utf-8', $this->config->ldapencoding);
346
347
348
                }
            }
        }
skodak's avatar
skodak committed
349

350
351
        //Following sets all mandatory and other forced attribute values
        //User should be creted as login disabled untill email confirmation is processed
skodak's avatar
skodak committed
352
        //Feel free to add your user type and send patches to paca@sci.fi to add them
353
354
355
356
        //Moodle distribution

        switch ($this->config->user_type)  {
            case 'edir':
357
                $newuser['objectClass']   = array('inetOrgPerson', 'organizationalPerson', 'person', 'top');
skodak's avatar
skodak committed
358
                $newuser['uniqueId']      = $extusername;
359
                $newuser['logindisabled'] = 'TRUE';
skodak's avatar
skodak committed
360
                $newuser['userpassword']  = $extpassword;
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
                $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser);
                break;
            case 'rfc2307':
            case 'rfc2307bis':
                // posixAccount object class forces us to specify a uidNumber
                // and a gidNumber. That is quite complicated to generate from
                // Moodle without colliding with existing numbers and without
                // race conditions. As this user is supposed to be only used
                // with Moodle (otherwise the user would exist beforehand) and
                // doesn't need to login into a operating system, we assign the
                // user the uid of user 'nobody' and gid of group 'nogroup'. In
                // addition to that, we need to specify a home directory. We
                // use the root directory ('/') as the home directory, as this
                // is the only one can always be sure exists. Finally, even if
                // it's not mandatory, we specify '/bin/false' as the login
                // shell, to prevent the user from login in at the operating
                // system level (Moodle ignores this).

                $newuser['objectClass']   = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top');
                $newuser['cn']            = $extusername;
                $newuser['uid']           = $extusername;
                $newuser['uidNumber']     = AUTH_UID_NOBODY;
                $newuser['gidNumber']     = AUTH_GID_NOGROUP;
                $newuser['homeDirectory'] = '/';
                $newuser['loginShell']    = '/bin/false';

                // IMPORTANT:
                // We have to create the account locked, but posixAccount has
                // no attribute to achive this reliably. So we are going to
                // modify the password in a reversable way that we can later
                // revert in user_activate().
                //
                // Beware that this can be defeated by the user if we are not
                // using MD5 or SHA-1 passwords. After all, the source code of
                // Moodle is available, and the user can see the kind of
                // modification we are doing and 'undo' it by hand (but only
                // if we are using plain text passwords).
                //
                // Also bear in mind that you need to use a binding user that
                // can create accounts and has read/write privileges on the
                // 'userPassword' attribute for this to work.

                $newuser['userPassword']  = '*'.$extpassword;
                $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser);
405
406
407
408
409
410
                break;
            case 'ad':
                // User account creation is a two step process with AD. First you
                // create the user object, then you set the password. If you try
                // to set the password while creating the user, the operation
                // fails.
411

412
413
414
415
                // Passwords in Active Directory must be encoded as Unicode
                // strings (UCS-2 Little Endian format) and surrounded with
                // double quotes. See http://support.microsoft.com/?kbid=269190
                if (!function_exists('mb_convert_encoding')) {
416
                    print_error('auth_ldap_no_mbstring', 'auth_ldap');
417
                }
418

419
420
421
422
423
                // Check for invalid sAMAccountName characters.
                if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) {
                    print_error ('auth_ldap_ad_invalidchars', 'auth_ldap');
                }

424
                // First create the user account, and mark it as disabled.
425
                $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson');
426
                $newuser['sAMAccountName'] = $extusername;
427
                $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT |
428
                                                 AUTH_AD_ACCOUNTDISABLE;
429
                $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context;
430
                if (!ldap_add($ldapconnection, $userdn, $newuser)) {
431
                    print_error('auth_ldap_ad_create_req', 'auth_ldap');
432
                }
433

434
435
436
                // Now set the password
                unset($newuser);
                $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"',
437
                                                             'UCS-2LE', 'UTF-8');
438
439
440
                if(!ldap_modify($ldapconnection, $userdn, $newuser)) {
                    // Something went wrong: delete the user account and error out
                    ldap_delete ($ldapconnection, $userdn);
441
                    print_error('auth_ldap_ad_create_req', 'auth_ldap');
442
443
                }
                $uadd = true;
444
445
                break;
            default:
446
               print_error('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name);
447
        }
448
        $this->ldap_close();
449
450
451
        return $uadd;
    }

452
453
454
455
456
    /**
     * Returns true if plugin allows resetting of password from moodle.
     *
     * @return bool
     */
457
458
459
460
    function can_reset_password() {
        return !empty($this->config->stdchangepassword);
    }

461
462
463
464
465
    /**
     * Returns true if plugin allows signup and user creation.
     *
     * @return bool
     */
466
467
468
469
470
471
472
473
    function can_signup() {
        return (!empty($this->config->auth_user_create) and !empty($this->config->create_context));
    }

    /**
     * Sign up a new user ready for confirmation.
     * Password is passed in plaintext.
     *
skodak's avatar
skodak committed
474
     * @param object $user new user object
475
476
477
     * @param boolean $notify print notice with link and terminate
     */
    function user_signup($user, $notify=true) {
478
        global $CFG, $DB, $PAGE, $OUTPUT;
479

480
        require_once($CFG->dirroot.'/user/profile/lib.php');
481

482
        if ($this->user_exists($user->username)) {
483
            print_error('auth_ldap_user_exists', 'auth_ldap');
484
485
486
487
488
489
        }

        $plainslashedpassword = $user->password;
        unset($user->password);

        if (! $this->user_create($user, $plainslashedpassword)) {
490
            print_error('auth_ldap_create_error', 'auth_ldap');
491
492
        }

Petr Skoda's avatar
Petr Skoda committed
493
494
495
496
        if (!empty($CFG->defaultcity) and !property_exists($user, 'city')) {
            $user->city = $CFG->defaultcity;
        }

497
        $user->id = $DB->insert_record('user', $user);
498

499
        // Save any custom profile field information
500
501
        profile_save_data($user);

502
503
504
        $this->update_user_record($user->username);
        update_internal_user_password($user, $plainslashedpassword);

505
        $user = $DB->get_record('user', array('id'=>$user->id));
506
507
        events_trigger('user_created', $user);

508
        if (! send_confirmation_email($user)) {
509
            print_error('auth_emailnoemail', 'auth_email');
510
511
512
513
        }

        if ($notify) {
            $emailconfirm = get_string('emailconfirm');
514
            $PAGE->set_url('/auth/ldap/auth.php');
515
516
517
518
            $PAGE->navbar->add($emailconfirm);
            $PAGE->set_title($emailconfirm);
            $PAGE->set_heading($emailconfirm);
            echo $OUTPUT->header();
519
            notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php");
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
        } else {
            return true;
        }
    }

    /**
     * Returns true if plugin allows confirming of new users.
     *
     * @return bool
     */
    function can_confirm() {
        return $this->can_signup();
    }

    /**
     * Confirm the new user as registered.
     *
skodak's avatar
skodak committed
537
538
     * @param string $username
     * @param string $confirmsecret
539
540
     */
    function user_confirm($username, $confirmsecret) {
skodak's avatar
skodak committed
541
542
        global $DB;

543
544
545
546
547
548
        $user = get_complete_user_data('username', $username);

        if (!empty($user)) {
            if ($user->confirmed) {
                return AUTH_CONFIRM_ALREADY;

549
            } else if ($user->auth != $this->authtype) {
550
551
                return AUTH_CONFIRM_ERROR;

skodak's avatar
skodak committed
552
            } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in
553
554
555
                if (!$this->user_activate($username)) {
                    return AUTH_CONFIRM_FAIL;
                }
556
557
                $DB->set_field('user', 'confirmed', 1, array('id'=>$user->id));
                $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
558
559
560
561
562
563
564
                return AUTH_CONFIRM_OK;
            }
        } else {
            return AUTH_CONFIRM_ERROR;
        }
    }

565
    /**
566
     * Return number of days to user password expires
567
568
569
570
     *
     * If userpassword does not expire it should return 0. If password is already expired
     * it should return negative value.
     *
skodak's avatar
skodak committed
571
     * @param mixed $username username
572
573
574
     * @return integer
     */
    function password_expire($username) {
575
        $result = 0;
skodak's avatar
skodak committed
576
577

        $textlib = textlib_get_instance();
skodak's avatar
skodak committed
578
        $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
skodak's avatar
skodak committed
579

580
        $ldapconnection = $this->ldap_connect();
skodak's avatar
skodak committed
581
        $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
582
        $search_attribs = array($this->config->expireattr);
583
        $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
584
        if ($sr)  {
585
586
            $info = ldap_get_entries_moodle($ldapconnection, $sr);
            $info = array_change_key_case($info, CASE_LOWER);
587
588
589
590
591
592
593
594
595
596
            if (!empty ($info) and !empty($info[0][$this->config->expireattr][0])) {
                $expiretime = $this->ldap_expirationtime2unix($info[0][$this->config->expireattr][0], $ldapconnection, $user_dn);
                if ($expiretime != 0) {
                    $now = time();
                    if ($expiretime > $now) {
                        $result = ceil(($expiretime - $now) / DAYSECS);
                    }
                    else {
                        $result = floor(($expiretime - $now) / DAYSECS);
                    }
skodak's avatar
skodak committed
597
                }
598
            }
skodak's avatar
skodak committed
599
        } else {
600
            error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap'));
601
602
603
604
605
606
        }

        return $result;
    }

    /**
607
     * Syncronizes user fron external LDAP server to moodle user table
608
     *
skodak's avatar
skodak committed
609
610
     * Sync is now using username attribute.
     *
611
     * Syncing users removes or suspends users that dont exists anymore in external LDAP.
skodak's avatar
skodak committed
612
613
     * Creates new users and updates coursecreator status of users.
     *
614
     * @param bool $do_updates will do pull in data updates from LDAP if relevant
615
     */
skodak's avatar
skodak committed
616
617
    function sync_users($do_updates=true) {
        global $CFG, $DB;
618

619
620
621
        print_string('connectingldap', 'auth_ldap');
        $ldapconnection = $this->ldap_connect();

skodak's avatar
skodak committed
622
        $textlib = textlib_get_instance();
skodak's avatar
skodak committed
623
        $dbman = $DB->get_manager();
skodak's avatar
skodak committed
624

skodak's avatar
skodak committed
625
626
    /// Define table user to be created
        $table = new xmldb_table('tmp_extuser');
627
628
        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
        $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
629
        $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
skodak's avatar
skodak committed
630
631
        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
        $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username'));
632

633
634
        print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser');
        $dbman->create_temp_table($table);
635
636
637
638
639

        ////
        //// get user's list from ldap to sql in a scalable fashion
        ////
        // prepare some data we'll need
640
        $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
641

642
        $contexts = explode(';', $this->config->contexts);
skodak's avatar
skodak committed
643

644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
        if (!empty($this->config->create_context)) {
              array_push($contexts, $this->config->create_context);
        }

        $fresult = array();
        foreach ($contexts as $context) {
            $context = trim($context);
            if (empty($context)) {
                continue;
            }
            if ($this->config->search_sub) {
                //use ldap_search to find first user from subtree
                $ldap_result = ldap_search($ldapconnection, $context,
                                           $filter,
                                           array($this->config->user_attribute));
skodak's avatar
skodak committed
659
            } else {
660
661
662
663
664
665
                //search only in this context
                $ldap_result = ldap_list($ldapconnection, $context,
                                         $filter,
                                         array($this->config->user_attribute));
            }

666
667
668
669
670
            if(!$ldap_result) {
                continue;
            }

            if ($entry = @ldap_first_entry($ldapconnection, $ldap_result)) {
671
                do {
skodak's avatar
skodak committed
672
673
                    $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute);
                    $value = $textlib->convert($value[0], $this->config->ldapencoding, 'utf-8');
skodak's avatar
skodak committed
674
                    $this->ldap_bulk_insert($value);
skodak's avatar
skodak committed
675
                } while ($entry = ldap_next_entry($ldapconnection, $entry));
676
            }
skodak's avatar
skodak committed
677
            unset($ldap_result); // free mem
678
679
680
681
682
        }

        /// preserve our user database
        /// if the temp table is empty, it probably means that something went wrong, exit
        /// so as to avoid mass deletion of users; which is hard to undo
skodak's avatar
skodak committed
683
        $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}');
684
        if ($count < 1) {
685
            print_string('didntgetusersfromldap', 'auth_ldap');
686
            exit;
687
        } else {
688
            print_string('gotcountrecordsfromldap', 'auth_ldap', $count);
689
690
691
        }


skodak's avatar
skodak committed
692
/// User removal
693
        // Find users in DB that aren't in ldap -- to be removed!
skodak's avatar
skodak committed
694
        // this is still not as scalable (but how often do we mass delete?)
695
696
        if ($this->config->removeuser !== AUTH_REMOVEUSER_KEEP) {
            $sql = 'SELECT u.id, u.username, u.email, u.auth
skodak's avatar
skodak committed
697
                      FROM {user} u
698
699
700
701
702
                      LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
                     WHERE u.auth = ?
                           AND u.deleted = 0
                           AND e.username IS NULL';
            $remove_users = $DB->get_records_sql($sql, array($this->authtype));
skodak's avatar
skodak committed
703
704

            if (!empty($remove_users)) {
705
                print_string('userentriestoremove', 'auth_ldap', count($remove_users));
skodak's avatar
skodak committed
706
707

                foreach ($remove_users as $user) {
708
                    if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) {
709
                        if (delete_user($user)) {
710
                            echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
skodak's avatar
skodak committed
711
                        } else {
712
                            echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n";
skodak's avatar
skodak committed
713
                        }
714
                    } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
715
                        $updateuser = new stdClass();
skodak's avatar
skodak committed
716
717
                        $updateuser->id = $user->id;
                        $updateuser->auth = 'nologin';
718
719
                        $DB->update_record('user', $updateuser);
                        echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
skodak's avatar
skodak committed
720
                    }
721
                }
skodak's avatar
skodak committed
722
            } else {
723
                print_string('nouserentriestoremove', 'auth_ldap');
skodak's avatar
skodak committed
724
725
726
727
728
            }
            unset($remove_users); // free mem!
        }

/// Revive suspended users
729
        if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
skodak's avatar
skodak committed
730
            $sql = "SELECT u.id, u.username
skodak's avatar
skodak committed
731
                      FROM {user} u
732
733
734
                      JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid)
                     WHERE u.auth = 'nologin' AND u.deleted = 0";
            $revive_users = $DB->get_records_sql($sql);
skodak's avatar
skodak committed
735
736

            if (!empty($revive_users)) {
737
                print_string('userentriestorevive', 'auth_ldap', count($revive_users));
skodak's avatar
skodak committed
738
739

                foreach ($revive_users as $user) {
740
                    $updateuser = new stdClass();
skodak's avatar
skodak committed
741
                    $updateuser->id = $user->id;
742
                    $updateuser->auth = $this->authtype;
743
744
                    $DB->update_record('user', $updateuser);
                    echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
745
                }
skodak's avatar
skodak committed
746
            } else {
747
                print_string('nouserentriestorevive', 'auth_ldap');
skodak's avatar
skodak committed
748
749
750
            }

            unset($revive_users);
751
        }
752

skodak's avatar
skodak committed
753
754

/// User Updates - time-consuming (optional)
755
        if ($do_updates) {
756
            // Narrow down what fields we need to update
757
758
759
            $all_keys = array_keys(get_object_vars($this->config));
            $updatekeys = array();
            foreach ($all_keys as $key) {
760
761
                if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) {
                    // If we have a field to update it from
skodak's avatar
skodak committed
762
                    // and it must be updated 'onlogin' we
763
                    // update it on cron
764
                    if (!empty($this->config->{'field_map_'.$match[1]})
skodak's avatar
skodak committed
765
                         and $this->config->{$match[0]} === 'onlogin') {
766
767
768
769
770
                        array_push($updatekeys, $match[1]); // the actual key name
                    }
                }
            }
            unset($all_keys); unset($key);
skodak's avatar
skodak committed
771

772
        } else {
773
            print_string('noupdatestobedone', 'auth_ldap');
774
        }
775
776
        if ($do_updates and !empty($updatekeys)) { // run updates only if relevant
            $users = $DB->get_records_sql('SELECT u.username, u.id
skodak's avatar
skodak committed
777
                                             FROM {user} u
778
779
                                            WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?',
                                          array($this->authtype, $CFG->mnet_localhost_id));
780
            if (!empty($users)) {
781
                print_string('userentriestoupdate', 'auth_ldap', count($users));
skodak's avatar
skodak committed
782

783
                $sitecontext = get_context_instance(CONTEXT_SYSTEM);
skodak's avatar
skodak committed
784
                if (!empty($this->config->creators) and !empty($this->config->memberattribute)
785
                  and $roles = get_archetype_roles('coursecreator')) {
skodak's avatar
skodak committed
786
787
788
789
                    $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
                } else {
                    $creatorrole = false;
                }
790

791
                $transaction = $DB->start_delegated_transaction();
skodak's avatar
skodak committed
792
793
                $xcount = 0;
                $maxxcount = 100;
794

skodak's avatar
skodak committed
795
                foreach ($users as $user) {
796
                    echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id));
797
                    if (!$this->update_user_record($user->username, $updatekeys)) {
798
                        echo ' - '.get_string('skipped');
skodak's avatar
skodak committed
799
800
801
802
                    }
                    echo "\n";
                    $xcount++;

803
                    // Update course creators if needed
skodak's avatar
skodak committed
804
805
                    if ($creatorrole !== false) {
                        if ($this->iscreator($user->username)) {
806
                            role_assign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth);
skodak's avatar
skodak committed
807
                        } else {
808
                            role_unassign($creatorrole->id, $user->id, $sitecontext->id, $this->roleauth);
809
                        }
skodak's avatar
skodak committed
810
                    }
811
                }
812
                $transaction->allow_commit();
skodak's avatar
skodak committed
813
                unset($users); // free mem
814
            }
815
        } else { // end do updates
816
            print_string('noupdatestobedone', 'auth_ldap');
817
        }
skodak's avatar
skodak committed
818
819

/// User Additions
820
        // Find users missing in DB that are in LDAP
821
        // and gives me a nifty object I don't want.
skodak's avatar
skodak committed
822
        // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin
823
824
825
826
827
        $sql = 'SELECT e.id, e.username
                  FROM {tmp_extuser} e
                  LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid)
                 WHERE u.id IS NULL';
        $add_users = $DB->get_records_sql($sql);
skodak's avatar
skodak committed
828

829
        if (!empty($add_users)) {
830
            print_string('userentriestoadd', 'auth_ldap', count($add_users));
831

skodak's avatar
skodak committed
832
833
            $sitecontext = get_context_instance(CONTEXT_SYSTEM);
            if (!empty($this->config->creators) and !empty($this->config->memberattribute)
834
              and $roles = get_archetype_roles('coursecreator')) {
835
                $creatorrole = array_shift($roles);      // We can only use one, let's use the first one
skodak's avatar
skodak committed
836
837
            } else {
                $creatorrole = false;
838
839
            }

840
            $transaction = $DB->start_delegated_transaction();
841
            foreach ($add_users as $user) {
skodak's avatar
skodak committed
842
                $user = $this->get_userinfo_asobj($user->username);
skodak's avatar
skodak committed
843

844
                // Prep a few params
donal72's avatar
donal72 committed
845
846
                $user->modified   = time();
                $user->confirmed  = 1;
847
                $user->auth       = $this->authtype;
donal72's avatar
donal72 committed
848
                $user->mnethostid = $CFG->mnet_localhost_id;
849
850
851
                // get_userinfo_asobj() might have replaced $user->username with the value
                // from the LDAP server (which can be mixed-case). Make sure it's lowercase
                $user->username = trim(moodle_strtolower($user->username));
skodak's avatar
skodak committed
852
853
                if (empty($user->lang)) {
                    $user->lang = $CFG->lang;
854
                }
skodak's avatar
skodak committed
855

Petr Skoda's avatar
Petr Skoda committed
856
857
858
859
                if (!empty($CFG->defaultcity) and !property_exists($user, 'city')) {
                    $user->city = $CFG->defaultcity;
                }

860
861
862
863
864
                $id = $DB->insert_record('user', $user);
                echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
                if (!empty($this->config->forcechangepassword)) {
                    set_user_preference('auth_forcepasswordchange', 1, $id);
                }
865

866
867
868
                // Add course creators if needed
                if ($creatorrole !== false and $this->iscreator($user->username)) {
                    role_assign($creatorrole->id, $id, $sitecontext->id, $this->roleauth);
skodak's avatar
skodak committed
869
870
                }

871
            }
872
            $transaction->allow_commit();
873
            unset($add_users); // free mem
874
        } else {
875
            print_string('nouserstobeadded', 'auth_ldap');
876
        }
skodak's avatar
skodak committed
877
878

        $dbman->drop_temp_table($table);
879
        $this->ldap_close();
skodak's avatar
skodak committed
880

881
882
883
        return true;
    }

skodak's avatar
skodak committed
884
885
886
    /**
     * Update a local user record from an external source.
     * This is a lighter version of the one in moodlelib -- won't do
887
888
     * expensive ops such as enrolment.
     *
skodak's avatar
skodak committed
889
890
891
     * If you don't pass $updatekeys, there is a performance hit and
     * values removed from LDAP won't be removed from moodle.
     *
892
     * @param string $username username
893
     * @param boolean $updatekeys true to update the local record with the external LDAP values.
894
895
     */
    function update_user_record($username, $updatekeys = false) {
896
        global $CFG, $DB;
897

898
        // Just in case check text case
899
        $username = trim(moodle_strtolower($username));
skodak's avatar
skodak committed
900

901
        // Get the current user record
902
        $user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id));
903
        if (empty($user)) { // trouble
904
905
            error_log($this->errorlogtag.get_string('auth_dbusernotexist', 'auth_db', '', $username));
            print_error('auth_dbusernotexist', 'auth_db', '', $username);
906
907
908
            die;
        }

donal72's avatar
donal72 committed
909
910
911
        // Protect the userid from being overwritten
        $userid = $user->id;

skodak's avatar
skodak committed
912
913
914
915
916
917
918
919
920
921
922
923
        if ($newinfo = $this->get_userinfo($username)) {
            $newinfo = truncate_userinfo($newinfo);

            if (empty($updatekeys)) { // all keys? this does not support removing values
                $updatekeys = array_keys($newinfo);
            }

            foreach ($updatekeys as $key) {
                if (isset($newinfo[$key])) {
                    $value = $newinfo[$key];
                } else {
                    $value = '';
924
                }
skodak's avatar
skodak committed
925
926
927

                if (!empty($this->config->{'field_updatelocal_' . $key})) {
                    if ($user->{$key} != $value) { // only update if it's changed
928
                        $DB->set_field('user', $key, $value, array('id'=>$userid));
929
930
931
                    }
                }
            }
skodak's avatar
skodak committed
932
933
        } else {
            return false;
934
        }
935
        return $DB->get_record('user', array('id'=>$userid, 'deleted'=>0));
936
937
    }

skodak's avatar
skodak committed
938
939
940
    /**
     * Bulk insert in SQL's temp table
     */
skodak's avatar
skodak committed
941
    function ldap_bulk_insert($username) {
942
        global $DB, $CFG;
skodak's avatar
skodak committed
943
944

        $username = moodle_strtolower($username); // usernames are __always__ lowercase.
945
946
947
        $DB->insert_record_raw('tmp_extuser', array('username'=>$username,
                                                    'mnethostid'=>$CFG->mnet_localhost_id), false, true);
        echo '.';
948
949
    }

skodak's avatar
skodak committed
950
    /**
951
     * Activates (enables) user in external LDAP so user can login
952
     *
skodak's avatar
skodak committed
953
     * @param mixed $username
954
     * @return boolean result