lib.php 119 KB
Newer Older
1
2
3
4
<?php
/**
 *
 * @package    mahara
Nigel McNie's avatar
Nigel McNie committed
5
 * @subpackage auth
6
 * @author     Catalyst IT Ltd
7
8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9
10
11
12
13
 *
 */

defined('INTERNAL') || die();

14
require(get_config('docroot') . 'auth/user.php');
15
require_once(get_config('docroot') . '/lib/htmloutput.php');
16

Nigel McNie's avatar
Nigel McNie committed
17
18
19
/**
 * Unknown user exception
 */
20
class AuthUnknownUserException extends UserException {}
21

22
/**
Aaron Wells's avatar
Aaron Wells committed
23
 * An instance of an auth plugin failed during execution
24
25
26
27
28
29
 * e.g. LDAP auth failed to connect to a directory
 * Developers can use this to fail an individual auth
 * instance, but not kill all from being tried.
 * If appropriate - the 'message' of the exception will be used
 * as the display message, so don't forget to language translate it
 */
30
31
32
33
34
35
class AuthInstanceException extends UserException {

    public function strings() {
        return array_merge(parent::strings(),
                           array('title' => $this->get_sitename() . ': Authentication problem'));
    }
36
37
38
39

    public function render_exception() {
        return $this->get_string('message') . "\n\n" . preg_replace('/<br\s?\/?>/ius', "\n", $this->getMessage());
    }
40
}
41

42
/**
Aaron Wells's avatar
Aaron Wells committed
43
 * We tried to call a method on an auth plugin that hasn't been init'ed
44
45
46
47
 * successfully
 */
class UninitialisedAuthException extends SystemException {}

48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
 * We tried creating automatically creating an account for a user but
 * it failed for a reason that the user might want to know about
 * (e.g. they used an email address that's already used on the site)
 */
class AccountAutoCreationException extends AuthInstanceException {

    public function strings() {
        return array_merge(parent::strings(),
                           array('message' => 'The automatic creation of your user account failed.'
                                 . "\nDetails if any, follow:"));
    }
}

62
63
64
/**
 * Base authentication class. Provides a common interface with which
 * authentication can be carried out for system users.
Nigel McNie's avatar
Nigel McNie committed
65
66
67
68
69
70
 *
 * @todo for authentication:
 *   - inactivity: each institution has inactivity timeout times, this needs
 *     to be supported
 *     - this means the lastlogin field needs to be updated on the usr table
 *     - warnings are handled by cron
71
72
73
 */
abstract class Auth {

74
75
76
77
78
    protected $instanceid;
    protected $institution;
    protected $instancename;
    protected $priority;
    protected $authname;
79
    protected $active;
80
    protected $config;
81
    protected $has_instance_config;
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
    protected $type;
    protected $ready;

    /**
     * Given an id, create the auth object and retrieve the config settings
     * If an instance ID is provided, get all the *instance* config settings
     *
     * @param  int  $id   The unique ID of the auth instance
     * @return bool       Whether the create was successful
     */
    public function __construct($id = null) {
        $this->ready = false;
    }

    /**
     * Instantiate the plugin by pulling the config data for an instance from
     * the database
     *
     * @param  int  $id   The unique ID of the auth instance
     * @return bool       Whether the create was successful
     */
    public function init($id) {
        if (!is_numeric($id) || intval($id) != $id) {
            throw new UserNotFoundException();
        }

        $instance = get_record('auth_instance', 'id', $id);
        if (empty($instance)) {
            throw new UserNotFoundException();
        }

        $this->instanceid   = $id;
        $this->institution  = $instance->institution;
        $this->instancename = $instance->instancename;
        $this->priority     = $instance->priority;
117
        $this->active       = (isset($instance->active) ? $instance->active : 1); // We need to check if column exists yet via upgrade and set a default if not
118
119
        $this->authname     = $instance->authname;

Aaron Wells's avatar
Aaron Wells committed
120
        // Return now if the plugin type doesn't require any config
121
        // (e.g. internal)
122
        if ($this->has_instance_config == false) {
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
            return true;
        }

        $records = get_records_array('auth_instance_config', 'instance', $this->instanceid);

        if ($records == false) {
            return false;
        }

        foreach($records as $record) {
            $this->config[$record->field] = $record->value;
        }

        return true;
    }

    /**
     * The __get overloader is invoked when the requested member is private or
     * protected, or just doesn't exist.
Aaron Wells's avatar
Aaron Wells committed
142
     *
143
144
145
146
     * @param  string  $name   The name of the value to fetch
     * @return mixed           The value
     */
    public function __get($name) {
147
        $approved_members = array('instanceid', 'institution', 'instancename', 'priority', 'authname', 'type');
148
149
150
151

        if (in_array($name, $approved_members)) {
            return $this->{$name};
        }
152
153
        if (isset($this->config[$name])) {
            return $this->config[$name];
154
155
156
157
158
159
160
        }
        return null;
    }

    /**
     * The __set overloader is invoked when the specified member is private or
     * protected, or just doesn't exist.
Aaron Wells's avatar
Aaron Wells committed
161
     *
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
     * @param  string  $name   The name of the value to set
     * @param  mixed   $value  The value to assign
     * @return void
     */
    public function __set($name, $value) {
        /*
        if (property_exists($this, $name)) {
            $this->{$name} = $value;
            return;
        }
        */
        throw new SystemException('It\'s forbidden to set values on Auth objects');
    }

    /**
     * Check that the plugin has been initialised before we try to use it.
Aaron Wells's avatar
Aaron Wells committed
178
     *
179
     * @throws UninitialisedAuthException
Aaron Wells's avatar
Aaron Wells committed
180
     * @return bool
181
182
183
184
185
186
187
188
189
190
191
     */
    protected function must_be_ready() {
        if ($this->ready == false) {
            throw new UninitialisedAuthException('This Auth plugin has not been initialised');
        }
        return true;
    }

    /**
     * Fetch the URL that users can visit to change their passwords. This might
     * be a Moodle installation, for example.
Aaron Wells's avatar
Aaron Wells committed
192
     *
193
194
195
196
197
198
199
200
201
202
     * @return  mixed   URL to change password or false if there is none
     */
    public function changepasswordurl() {
        $this->must_be_ready();
        if (empty($this->config['changepasswordurl'])) {
            return false;
        }
        return $this->config['changepasswordurl'];
    }

203
    /**
204
     * Given a username and password, attempts to log the user in.
205
     *
206
     * @param object $user      An object with username member (at least)
207
208
209
210
211
     * @param string $password  The password to use for the attempt
     * @return bool             Whether the authentication was successful
     * @throws AuthUnknownUserException  If the user is unknown to the
     *                                   authentication method
     */
212
213
214
215
    public function authenticate_user_account($user, $password) {
        $this->must_be_ready();
        return false;
    }
216

217
218
219
220
221
222
    /**
     * Given a username, returns whether the user exists in the usr table
     *
     * @param string $username The username to attempt to identify
     * @return bool            Whether the username exists
     */
223
224
    public function user_exists($username) {
        $this->must_be_ready();
225
        if (record_exists_select('usr', 'LOWER(username) = ?', array(strtolower($username)))) {
226
227
228
229
            return true;
        }
        throw new AuthUnknownUserException("\"$username\" is not known to Auth");
    }
230

231
    /**
232
     * Returns whether the authentication instance can automatically create a
233
234
     * user record.
     *
235
     * Auto creating users means that the authentication plugin can say that
236
237
238
239
     * users who don't exist yet in Mahara's usr table are allowed, and Mahara
     * should create a user account for them. Example: the first time a user logs
     * in, when authenticating against an ldap store or similar).
     *
240
241
242
     * However, if a plugin says a user can be authenticated, then it must
     * implement the get_user_info() method which will be called to find out
     * information about the user so a record in the usr table _can_ be created
243
244
     * for the new user.
     *
245
246
     * Authentication methods must implement this method. Some may choose to
     * implement it by returning an instance config value that the admin user
247
248
249
250
251
252
     * can set.
     *
     * @return bool
     */
    public abstract function can_auto_create_users();

253
254
255
256
257
258
259
260
261
    /**
     * If this plugin allows new user's to self-register, this function will be
     * called to check whether it is okay to display a captcha method on the new
     * user self-registration form.
     */
    public static function can_use_registration_captcha() {
        return true;
    }

262
    /**
263
     * Given a username, returns a hash of information about a user from the
264
     * external data source.
265
266
267
268
269
270
     *
     * @param string $username The username to look up information for
     * @return array           The information for the user
     * @throws AuthUnknownUserException If the user is unknown to the
     *                                  authentication method
     */
271
272
273
    public function get_user_info($username) {
        return false;
    }
274

Nigel McNie's avatar
Nigel McNie committed
275
276
277
278
    /**
     * Given a password, returns whether it is in a valid format for this
     * authentication method.
     *
279
280
281
282
283
     * This only needs to be defined by subclasses if:
     *  - They implement the change_password method, which means that the
     *    system can use the <kbd>passwordchange</kbd> flag on the <kbd>usr</kbd>
     *    table to control whether the user's password needs changing.
     *  - The password that a user can set must be in a certain format.
Nigel McNie's avatar
Nigel McNie committed
284
285
286
287
     *
     * The default behaviour is to assume that the password is in a valid form,
     * so make sure to implement this method if this is not the case!
     *
Aaron Wells's avatar
Aaron Wells committed
288
     * This method is defined to be empty, so that authentication methods do
289
290
     * not have to specify a format if they do not need to.
     *
Nigel McNie's avatar
Nigel McNie committed
291
292
293
     * @param string $password The password to check
     * @return bool            Whether the username is in valid form.
     */
294
    public function is_password_valid($password) {
Nigel McNie's avatar
Nigel McNie committed
295
296
297
        return true;
    }

298
299
300
301
302
303
304
305
306
307
308
309
    /**
     * Called when a user is being logged in, after the main authentication routines.
     *
     * You can use $USER->login() to perform any additional tasks, for example
     * to set a cookie that another application can read, or pull some data
     * from somewhere.
     *
     * This method has no parameters and needs no return value
     */
    public function login() {
    }

310
    /**
Aaron Wells's avatar
Aaron Wells committed
311
312
313
     * Called when a user is being logged out, either by clicking a logout
     * link, their session timing out or some other method where their session
     * is explicitly being ended with no more processing to take place on this
314
315
     * page load.
     *
Aaron Wells's avatar
Aaron Wells committed
316
317
     * You can use $USER->logout() to log a user out but continue page
     * processing if necessary. register.php is an example of such a place
318
319
     * where this happens.
     *
Aaron Wells's avatar
Aaron Wells committed
320
321
     * If you define this hook, you can call $USER->logout() in it if you need
     * to before redirecting. Otherwise, it will be called for you once your
322
323
     * hook has run.
     *
Aaron Wells's avatar
Aaron Wells committed
324
325
     * If you do not explicitly redirect yourself, once this hook is finished
     * the user will be redirected to the homepage with a message saying they
326
327
328
329
330
331
332
     * have been logged out successfully.
     *
     * This method has no parameters and needs no return value
     */
    public function logout() {
    }

333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
    /**
     * Indicates whether this auth instance is parent to another auth instance
     * @return boolean (For backwards-compatibility reasons, it actually returns $this or null)
     */
    public function is_parent_authority() {
        if (count_records('auth_instance_config', 'field', 'parent', 'value', $this->instanceid)) {
            return $this;
        }
        else {
            return null;
        }
    }

    /**
     * Returns the ID of this instance's parent authority; or FALSE if it has no parent authority
     * @return int|false
     */
    public function get_parent_authority() {
        return get_field('auth_instance_config', 'value', 'instance', $this->id, 'field', 'parent');
    }


    /**
     * Indicates whether or not this auth instance uses the remote username. Most auth instances
     * will only use it if they are the parent to another auth instance.
     */
    public function needs_remote_username() {
        return (boolean) $this->is_parent_authority();
    }
362
363
364
}


365
366
367
/******************************************************************************/
    // End of Auth base-class
/******************************************************************************/
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
/*
* Checks all the available auth types and executes 'install_auth_default' method
* if they have one
*/
function install_auth_default() {
    $plugins = auth_get_available_auth_types();

    foreach ($plugins as $key => $value) {
        $classname = 'PluginAuth' . ucfirst(strtolower($value->name));
        $methodname = 'install_auth_default';
        if (method_exists($classname, $methodname)) {
            call_static_method($classname, $methodname);
        }
    }
}
383

384
/**
Aaron Wells's avatar
Aaron Wells committed
385
 * Handles authentication by setting up a session for a user if they are logged
386
 * in.
387
388
389
390
391
392
393
394
 *
 * This function combined with the Session class is smart - if the user is not
 * logged in then they do not get a session, which prevents simple curl hits
 * or search engine crawls to a page from getting sessions they won't use.
 *
 * Once the user has a session, they keep it even if the log out, so it can
 * be reused. The session does expire, but the expiry time is typically a week
 * or more.
Nigel McNie's avatar
Nigel McNie committed
395
396
397
398
399
400
401
 *
 * If the user is not authenticated for this page, then this function will
 * exit, printing the login page. Therefore, after including init.php, you can
 * be sure that the user is logged in, or has a valid guest key. However, no
 * testing is done to make sure the user has the required permissions to see
 * the page.
 *
402
403
 */
function auth_setup () {
404
    global $SESSION, $USER;
405
406
407
408

    // If the system is not installed, let the user through in the hope that
    // they can fix this little problem :)
    if (!get_config('installed')) {
409
        $USER->logout();
410
411
412
413
414
        return;
    }

    // Check the time that the session is set to log out. If the user does
    // not have a session, this time will be 0.
415
    $sessionlogouttime = $USER->get('logout_time');
416
417
418
419

    // Need to doublecheck that the User's sessionid still has a match the usr_session table
    // It can disappear if the current user has hacked the real user's account and the real user has
    // reset the password clearing the session from usr_session.
420
    $sessionexists = ($USER->id > 0) ? get_record('usr_session', 'usr', $USER->id, 'session', $USER->get('sessionid')) : false;
421
    $parentuser = $USER->get('parentuser');
422
    if (($sessionlogouttime && param_exists('logout'))
423
424
       || ($sessionexists === false && $USER->get('sessionid') != '' && empty($parentuser))
       || ($sessionexists && isset($sessionexists->useragent) && $sessionexists->useragent != $_SERVER['HTTP_USER_AGENT'])) {
425
426
        // Call the authinstance' logout hook
        $authinstance = $SESSION->get('authinstance');
427
428
429
430
431
432
433
        if ($authinstance) {
            $authobj = AuthFactory::create($authinstance);
            $authobj->logout();
        }
        else {
            log_debug("Strange: user " . $USER->get('username') . " had no authinstance set in their session");
        }
434

Richard Mansfield's avatar
Richard Mansfield committed
435
436
437
438
        if (function_exists('local_logout')) {
            local_logout();
        }

439
440
        $USER->logout();
        $SESSION->add_ok_msg(get_string('loggedoutok'));
441
        redirect();
Nigel McNie's avatar
Nigel McNie committed
442
443
    }
    if ($sessionlogouttime > time()) {
444
        // The session is still active, so continue it.
445
446
        // Make sure that if a user's admin status has changed, they're kicked
        // out of the admin section
447
448
449
450
        if (in_admin_section()) {
            // Reload site admin/staff permissions
            $realuser = get_record('usr', 'id', $USER->id, null, null, null, null, 'admin,staff');
            if (!$USER->get('admin') && $realuser->admin) {
451
                // The user has been made into an admin
452
                $USER->admin = 1;
453
            }
454
            else if ($USER->get('admin') && !$realuser->admin) {
455
                // The user's admin rights have been taken away
456
                $USER->admin = 0;
457
            }
458
459
            if (!$USER->get('staff') && $realuser->staff) {
                $USER->staff = 1;
460
            }
461
462
            else if ($USER->get('staff') && !$realuser->staff) {
                $USER->staff = 0;
463
            }
464
465
466
            // Reload institutional admin/staff permissions
            $USER->reset_institutions();
            auth_check_admin_section();
467
        }
468
        $USER->renew();
469
        auth_check_required_fields();
470
471
472
    }
    else if ($sessionlogouttime > 0) {
        // The session timed out
473
474

        $authinstance = $SESSION->get('authinstance');
475
476
477
478
479
        if ($authinstance) {
            $authobj = AuthFactory::create($authinstance);

            $mnetuser = 0;
            if ($SESSION->get('mnetuser') && $authobj->parent) {
Aaron Wells's avatar
Aaron Wells committed
480
                // We wish to remember that the user is an MNET user - even though
481
482
483
                // they're using the local login form
                $mnetuser = $USER->get('id');
            }
484

485
486
            $authobj->logout();
            $USER->logout();
487

488
489
490
491
492
493
494
            if ($mnetuser != 0) {
                $SESSION->set('mnetuser', $mnetuser);
                $SESSION->set('authinstance', $authinstance);
            }
        }
        else {
            log_debug("Strange: user " . $USER->get('username') . " had no authinstance set in their session");
495
496
        }

497
        if (defined('JSON')) {
498
            json_reply('global', get_string('sessiontimedoutreload'), 1);
499
500
        }

501
502
503
504
505
506
        // If the page the user is viewing is public, inform them that they can
        // log in again
        if (defined('PUBLIC')) {
            // @todo this links to ?login - later it should do magic to make
            // sure that whatever GET string is made it includes the old data
            // correctly
507
508
509
510
            $loginurl = $_SERVER['REQUEST_URI'];
            $loginurl .= (false === strpos($loginurl, '?')) ? '?' : '&';
            $loginurl .= 'login';
            $SESSION->add_info_msg(get_string('sessiontimedoutpublic', 'mahara', hsc($loginurl)), false);
511
512
513
            return;
        }

514
        auth_draw_login_page(get_string('sessiontimedout'));
515
516
517
    }
    else {
        // There is no session, so we check to see if one needs to be started.
518
        // Build login form. If the form is submitted it will be handled here,
519
520
        // and set $USER for us (this will happen when users hit a page and
        // specify login data immediately
521
        $form = pieform_instance(auth_get_login_form());
522
523
        if ($USER->is_logged_in()) {
            return;
524
        }
Aaron Wells's avatar
Aaron Wells committed
525

526
        // Check if the page is public or the site is configured to be public.
527
        if (defined('PUBLIC') && !param_exists('login')) {
528
            return;
529
        }
530
531
532
533
534

        // No session and a json request
        if (defined('JSON')) {
            json_reply('global', get_string('nosessionreload'), 1);
        }
Aaron Wells's avatar
Aaron Wells committed
535

536
        auth_draw_login_page(null, $form);
537
        exit;
538
539
540
    }
}

541
/**
Aaron Wells's avatar
Aaron Wells committed
542
 *
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
 * Returns all auth instances
 *
 * @return array                     Array of auth instance records
 */
function auth_get_auth_instances() {
    static $cache = array();

    if (count($cache) > 0) {
        return $cache;
    }

    $sql ='
        SELECT DISTINCT
            i.id,
            inst.name,
            inst.displayname,
559
            i.instancename,
560
561
            i.authname,
            i.active
Aaron Wells's avatar
Aaron Wells committed
562
        FROM
563
564
            {institution} inst,
            {auth_instance} i
Aaron Wells's avatar
Aaron Wells committed
565
        WHERE
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
            i.institution = inst.name
        ORDER BY
            inst.displayname,
            i.instancename';

    $cache = get_records_sql_array($sql, array());

    if (empty($cache)) {
        return array();
    }

    return $cache;
}


581
/**
Aaron Wells's avatar
Aaron Wells committed
582
 *
583
584
585
586
587
588
589
590
591
592
593
594
595
 * Given a list of institutions, returns all auth instances associated with them
 *
 * @return array                     Array of auth instance records
 */
function auth_get_auth_instances_for_institutions($institutions) {
    if (empty($institutions)) {
        return array();
    }
    $sql ='
        SELECT DISTINCT
            i.id,
            inst.name,
            inst.displayname,
596
            i.instancename,
597
598
            i.authname,
            i.active
Aaron Wells's avatar
Aaron Wells committed
599
        FROM
600
601
            {institution} inst,
            {auth_instance} i
Aaron Wells's avatar
Aaron Wells committed
602
        WHERE
603
604
605
606
607
608
609
610
            i.institution = inst.name AND
            inst.name IN (' . join(',', array_map('db_quote',$institutions)) . ')
        ORDER BY
            inst.displayname,
            i.instancename';

    return get_records_sql_array($sql, array());
}
611
612


613
/**
Aaron Wells's avatar
Aaron Wells committed
614
 * Given an institution, returns the authentication methods used by it, sorted
615
 * by priority.
616
 *
617
618
 * @param  string   $institution     Name of the institution
 * @return array                     Array of auth instance records
619
 */
620
function auth_get_auth_instances_for_institution($institution=null) {
Nigel McNie's avatar
Nigel McNie committed
621
    static $cache = array();
622

623
    if (null == $institution) {
624
625
626
        return array();
    }

627
    if (!isset($cache[$institution])) {
628
629
630
631
632
633
634
635
636
637
638
        // Get auth instances in order of priority
        // DO NOT CHANGE THE SORT ORDER OF THIS RESULT SET
        // YEAH EINSTEIN - THAT MEANS YOU!!!

        // TODO: work out why this won't accept a placeholder - had to use db_quote
        $sql ='
            SELECT DISTINCT
                i.id,
                i.instancename,
                i.priority,
                i.authname,
639
                i.active,
640
641
                a.requires_config,
                a.requires_parent
Aaron Wells's avatar
Aaron Wells committed
642
            FROM
643
644
                {auth_instance} i,
                {auth_installed} a
Aaron Wells's avatar
Aaron Wells committed
645
            WHERE
646
647
648
649
650
651
652
653
                a.name = i.authname AND
                i.institution = '. db_quote($institution).'
            ORDER BY
                i.priority,
                i.instancename';

        $cache[$institution] = get_records_sql_array($sql, array());

654
        if (empty($cache[$institution])) {
655
656
            return false;
        }
Nigel McNie's avatar
Nigel McNie committed
657
    }
658
659

    return $cache[$institution];
660
661
}

662
663
/**
 * Given a wwwroot, find any auth instances that can come from that host
Aaron Wells's avatar
Aaron Wells committed
664
 *
665
666
667
 * @param   string  wwwroot of the host that is connecting to us
 * @return  array   array of record objects
 */
668
669
function auth_get_auth_instances_for_wwwroot($wwwroot) {
    $query = "  SELECT
670
671
                    ai.id,
                    ai.authname,
672
                    ai.active,
673
674
675
                    i.id as institutionid,
                    i.displayname,
                    i.suspended
676
                FROM
677
678
679
680
681
682
                    {auth_instance} ai
                    INNER JOIN {institution} i
                        ON ai.institution = i.name
                    INNER JOIN {auth_instance_config} aic
                        ON aic.field = 'wwwroot'
                        AND aic.instance = ai.id
683
                WHERE
684
                    aic.value = ?";
685
686
687
688

    return get_records_sql_array($query, array('value' => $wwwroot));
}

689
/**
Aaron Wells's avatar
Aaron Wells committed
690
 * Given an institution, get all the auth types EXCEPT those that are already
691
692
693
694
695
 * enabled AND do not require configuration.
 *
 * @param  string   $institution     Name of the institution
 * @return array                     Array of auth instance records
 */
696
function auth_get_available_auth_types($institution=null) {
697

698
    if (!is_null($institution) && (!is_string($institution) || strlen($institution) > 255)) {
699
700
701
702
703
704
705
706
        return array();
    }

    // TODO: work out why this won't accept a placeholder - had to use db_quote
    $sql ='
        SELECT DISTINCT
            a.name,
            a.requires_config
Aaron Wells's avatar
Aaron Wells committed
707
        FROM
708
            {auth_installed} a
Aaron Wells's avatar
Aaron Wells committed
709
        LEFT JOIN
710
            {auth_instance} i
Aaron Wells's avatar
Aaron Wells committed
711
        ON
712
713
714
715
716
717
718
            a.name = i.authname AND
            i.institution = '. db_quote($institution).'
        WHERE
           (a.requires_config = 1 OR
            i.id IS NULL) AND
            a.active = 1
        ORDER BY
Aaron Wells's avatar
Aaron Wells committed
719
            a.name';
720

721
722
723
724
725
    if (is_null($institution)) {
        $result = get_records_array('auth_installed', '','','name','name, requires_config');
    } else {
        $result = get_records_sql_array($sql, array());
    }
726
727
728
729
730

    if (empty($result)) {
        return array();
    }

731
732
733
734
735
736
737
738
739
740
    foreach ($result as &$row) {
        $row->title       = get_string('title', 'auth.' . $row->name);
        safe_require('auth', $row->name);
        if ($row->is_usable = call_static_method('PluginAuth' . $row->name, 'is_usable')) {
            $row->description = get_string('description', 'auth.' . $row->name);
        }
        else {
            $row->description = get_string('notusable', 'auth.' . $row->name);
        }
    }
741
    usort($result, create_function('$a, $b', 'if ($a->is_usable != $b->is_usable) return $b->is_usable; return strnatcasecmp($a->title, $b->title);'));
742

743
744
    return $result;
}
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
/**
 * Build the agree with or withdraw consent to privacy statement
 *
 * @param ignoreagreevalue true when a new privacy statement needs to be accepted,
 * false when the form will be displayed to allow the consent withdraw.
 * @return form
 */
function privacy_form($ignoreagreevalue = false) {
    global $USER;

    // Get all institutions of a user.
    $userinstitutions = array_keys($USER->get('institutions'));
    // Include the 'mahara' institution so that we may show the site privacy statement as well.
    array_push($userinstitutions, 'mahara');

    // Check if there are new privacies that need to be accepted.
    $latestversions = get_latest_privacy_versions($userinstitutions, $ignoreagreevalue);
    if (empty($latestversions)) {
        // We may be masquerading as user
        return '<div>' . get_string('noprivacystatementsaccepted', 'account') . '</div>';
    }

    foreach ($latestversions as $privacy) {
768
769
770
        if ($privacy->type == 'privacy') {
            $title = get_string('institutionprivacystatement', 'admin');
            if ($privacy->institution == 'mahara') {
771
                $title = get_string('siteprivacy', 'admin');
772
773
774
            }
        }
        else {
775
            $title = get_string('institutiontermsandconditions', 'admin');
776
            if ($privacy->institution == 'mahara') {
777
                $title = get_string('sitetermsandconditions', 'admin');
778
779
            }
        }
780
781
        $smarty = smarty_core();
        $smarty->assign('privacy', $privacy);
782
        $smarty->assign('privacytitle', $title);
783
784
785
786
787
        $smarty->assign('privacytime', format_date(strtotime($privacy->ctime)));
        $smarty->assign('ignoreagreevalue', $ignoreagreevalue);
        $htmlbegin = $smarty->fetch('privacy_panel_begin.tpl');

        //Build form elements.
788
        $elements[$privacy->institution . $privacy->type . 'text'] = array(
789
790
791
            'type' => 'markup',
            'value' => $htmlbegin,
        );
792
        $elements[$privacy->institution . $privacy->type . 'id'] = array(
793
794
795
            'type' => 'hidden',
            'value' => $privacy->id,
        );
796
797

        $elements[$privacy->institution . $privacy->type] = array(
798
            'type'         => 'switchbox',
799
800
801
            'title'        => get_string('privacyagreement', 'admin', get_string($privacy->type . 'lowcase', 'admin')),
            'description'  => $privacy->agreed ? get_string('privacyagreedto', 'admin',
                get_string($privacy->type . 'lowcase', 'admin'), format_date(strtotime($privacy->agreedtime))) : '',
802
803
804
805
            'defaultvalue' => $privacy->agreed ? true : false,
            'disabled'     => ($privacy->agreed && $ignoreagreevalue) ? true : false,
            'required' => true,
        );
806
        $elements[$privacy->institution . $privacy->type . 'switch'] = array(
807
808
809
            'type' => 'hidden',
            'value' => ($privacy->agreed && $ignoreagreevalue) ? 'disabled' : 'enabled',
        );
810

811
812
813
        $smarty = smarty_core();
        $smarty->assign('ignoreagreevalue', $ignoreagreevalue);
        $htmlend = $smarty->fetch('privacy_panel_end.tpl');
814
        $elements[$privacy->institution . $privacy->type . 'text2'] = array(
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
            'type' => 'markup',
            'value' => $htmlend,
        );

    }
    $classhidden = $ignoreagreevalue ? '' : 'js-hidden';
    $elements['submit'] = array(
        'class' => 'btn-primary ' . $classhidden,
        'type'  => 'submit',
        'value' => get_string('savechanges', 'admin')
    );
    $form = pieform(array(
        'name'       => 'agreetoprivacy',
        'elements' => $elements,
    ));
    return $form;
}
832
833
/**
 * Checks that all the required fields are set, and handles setting them if required.
834
835
836
 *
 * Checks whether the current user needs to change their password, and handles
 * the password changing if it's required.
837
838
 */
function auth_check_required_fields() {
839
    global $USER, $SESSION;
840

841
842
843
844
845
846
847
848
849
850
851
    // for the case we are mascarading as the user and we want to return to be admin user
    $restoreadmin = param_integer('restore', 0);
    $loginanyway = false;
    if ($USER->get('parentuser') && param_exists('loginanyway')) {
        $USER->loginanyway = true;
    }
    if ($USER->get('loginanyway')) {
        $loginanyway = true;
    }
    // Privacy statement.
    if (get_config('institutionstrictprivacy') && !$USER->has_latest_agreement() && !$restoreadmin && !$loginanyway) {
852
853
854
        // Build the agree with privacy statement form.
        $form = privacy_form(true);

855
        define('TITLE', get_string('legal', 'admin'));
856
857
        $smarty = smarty();
        setpageicon($smarty, 'icon-umbrella');
858
859
860
861
862
        if ($USER->get('parentuser')) {
            $smarty->assign('loginanyway',
            get_string('loginasoverrideprivacyaccept', 'admin',
                       '<strong><a class="" href="' . get_config('wwwroot') . '?loginanyway">', '</a></strong>'));
        }
863
        $smarty->assign('form', $form);
864
865
        $smarty->assign('description', get_string('newprivacy', 'admin'));
        $smarty->display('account/userprivacy.tpl');
866
867
868
        exit;
    }

869
    if (defined('NOCHECKREQUIREDFIELDS') || $SESSION->get('nocheckrequiredfields') === true) {
870
871
        return;
    }
872
873
874
875
876
877
878
879
880
881
    $changepassword = true;
    $elements = array();

    if (
        !$USER->get('passwordchange')                                // User doesn't need to change their password
        || ($USER->get('parentuser') && $USER->get('loginanyway'))   // User is masquerading and wants to log in anyway
        || defined('NOCHECKPASSWORDCHANGE')                          // The page wants to skip this hassle
        ) {
        $changepassword = false;
    }
882

883
    // Check if the user wants to log in anyway
884
    if ($USER->get('passwordchange') && $loginanyway) {
885
886
887
        $changepassword = false;
    }

888
889
890
891
892
    // Do not force password change on JSON request.
    if (defined('JSON') && JSON == true) {
        $changepassword = false;
    }

893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
    if ($changepassword) {
        $authobj = AuthFactory::create($USER->authinstance);

        if ($authobj->changepasswordurl) {
            redirect($authobj->changepasswordurl);
            exit;
        }

        if (method_exists($authobj, 'change_password')) {

            if ($SESSION->get('resetusername')) {
                $elements['username'] = array(
                    'type' => 'text',
                    'defaultvalue' => $USER->get('username'),
                    'title' => get_string('changeusername', 'account'),
                    'description' => get_string('changeusernamedesc', 'account', hsc(get_config('sitename'))),
                );
            }

            $elements['password1'] = array(
                'type'        => 'password',
                'title'       => get_string('newpassword') . ':',
Gregor Anzelj's avatar
Gregor Anzelj committed
915
916
                'description' => get_password_policy_description('user'),
                'showstrength' => true,
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
                'rules'       => array(
                    'required' => true
                )
            );

            $elements['password2'] = array(
                'type'        => 'password',
                'title'       => get_string('confirmpassword') . ':',
                'description' => get_string('yournewpasswordagain'),
                'rules'       => array(
                    'required' => true,
                ),
            );

            $elements['email'] = array(
                'type'   => 'text',
                'title'  => get_string('principalemailaddress', 'artefact.internal'),
                'ignore' => (trim($USER->get('email')) != '' && !preg_match('/@example\.org$/', $USER->get('email'))),
                'rules'  => array(
                    'required' => true,
                    'email'    => true,
                ),
            );
        }
    }
    else if (defined('JSON')) {
943
944
945
946
        // Don't need to check this for json requests
        return;
    }

947
948
    safe_require('artefact', 'internal');

949
    $alwaysmandatoryfields = array_keys(ArtefactTypeProfile::get_always_mandatory_fields());
950
    $element_data = ArtefactTypeProfile::get_field_element_data();
951
    foreach(ArtefactTypeProfile::get_mandatory_fields() as $field => $type) {
Aaron Wells's avatar
Aaron Wells committed
952
953
        // Always mandatory fields are stored in the usr table, so are part of
        // the user session object. We can save a query by grabbing them from
954
955
956
957
958
        // the session.
        if (in_array($field, $alwaysmandatoryfields) && $USER->get($field) != null) {
            continue;
        }
        // Not cached? Get value the standard way.
959
        if (get_profile_field($USER->get('id'), $field) != null) {
960
961
            continue;
        }
962
963
964
965
966
967
968
969
        // If profile field saves it's data somewhere different to normal
        $classname = 'ArtefactType' . ucfirst($field);
        if (is_callable(array($classname, 'defaultoption'))) {
            $option = call_static_method($classname, 'defaultoption');
            if (!empty($option)) {
                continue;
            }
        }
970

971
        if ($field == 'email') {
972
973
974
975
            if (isset($elements['email'])) {
                continue;
            }
            // Use a text field for their first e-mail address, not the
976
977
978
979
            // emaillist element
            $type = 'text';
        }

980
981
982
983
984
        $elements[$field] = array(
            'type'  => $type,
            'title' => get_string($field, 'artefact.internal'),
            'rules' => array('required' => true)
        );
985
986
987
988
989
        // We need to merge the rules for the element if they have special rules defined
        // in get_field_element_data() so that we save correct data.
        if (isset($element_data[$field])) {
            $elements[$field] = array_merge_recursive($elements[$field], $element_data[$field]);
        }
990

991
992
993
994
995
996
997
998
999
        if ($field == 'socialprofile') {
            $elements[$field] = ArtefactTypeSocialprofile::get_new_profile_elements();
            // add an element to flag that socialprofile is in the list of fields.
            $elements['socialprofile_hidden'] = array(
                'type'  => 'hidden',
                'value' => 1,
            );
        }

1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
        // @todo ruthlessly stolen from artefact/internal/index.php, could be merged
        if ($type == 'wysiwyg') {
            $elements[$field]['rows'] = 10;
            $elements[$field]['cols'] = 60;
        }
        if ($type == 'textarea') {
            $elements[$field]['rows'] = 4;
            $elements[$field]['cols'] = 60;
        }
        if ($field == 'country') {
            $elements[$field]['options'] = getoptions_country();
1011
            $elements[$field]['defaultvalue'] = get_config('country') ? get_config('country') : 'nz';
1012
        }
1013
1014
1015
1016
1017
1018
1019
1020
        if (is_callable(array($classname, 'getoptions'))) {
            $options = call_static_method($classname, 'getoptions');
            $elements[$field]['options'] = $options;
        }
        if (is_callable(array($classname, 'defaultoption'))) {
            $defaultoption = call_static_method($classname, 'defaultoption');
            $elements[$field]['defaultvalue'] = $defaultoption;
        }
1021
1022

        if ($field == 'email') {
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
            // Check if a validation email has been sent
            if (record_exists('artefact_internal_profile_email', 'owner', $USER->get('id'))) {
                $elements['email']['type'] = 'html';
                $elements['email']['value'] = get_string('validationprimaryemailsent', 'auth');
                $elements['email']['disabled'] = true;
                $elements['email']['rules'] = array('required' => false);
            }
            else {
                $elements[$field]['rules']['email'] = true;
                $elements[$field]['description'] = get_string('primaryemaildescription', 'auth');
            }
1034
        }
1035
1036
1037
    }

    if (empty($elements)) { // No mandatory fields that aren't set
1038
        $SESSION->set('nocheckrequiredfields', true);
1039
1040
1041
        return;
    }

1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
    if ((count($elements) == 1) && isset($elements['email']) && ($elements['email']['type'] == 'html')) {
        // Display a message if there is only 1 required field and this field is email whose validation has been sent
        $elements['submit'] = array(
                'type' => 'submit',
                'value' => get_string('continue', 'admin')
        );
        $form = pieform(array(
                'name'     => 'requiredfields',
                'method'   => 'post',
                'action'   => get_config('wwwroot') . '?logout',
                'elements' => $elements
        ));
    }
    else {
        $elements['submit'] = array(
            'type' => 'submit',
1058
            'class' => 'btn-primary',
1059
1060
            'value' => get_string('submit')
        );
1061

1062
1063
1064
1065
        $form = pieform(array(
            'name'     => 'requiredfields',
            'method'   => 'post',
            'action'   => '',
1066
1067
1068
            'elements' => $elements,
            'dieaftersubmit' => FALSE,
            'backoutaftersubmit' => TRUE,
1069
1070
        ));
    }
1071

1072
1073
1074
1075
1076
    // Has the form been successfully submitted? Back out and let the requested URL continue.
    if ($form === FALSE) {
        return;
    }

1077
    $smarty = smarty();
1078
1079
1080
    if ($USER->get('parentuser')) {
        $smarty->assign('loginasoverridepasswordchange',
            get_string('loginasoverridepasswordchange', 'admin',
1081
                       '<strong><a class="" href="' . get_config('wwwroot') . '?loginanyway">', '</a></strong>'));
1082
1083
    }
    $smarty->assign('changepassword', $changepassword);
1084
    $smarty->assign('changeusername', $SESSION->get('resetusername'));
1085
1086
1087
1088
1089
    $smarty->assign('form', $form);
    $smarty->display('requiredfields.tpl');
    exit;
}

1090
function requiredfields_validate(Pieform $form, $values) {
Nigel McNie's avatar
Nigel McNie committed
1091
    global $USER;
1092
    if (isset($values['password1'])) {
1093

1094
1095
1096
        // Get the authentication type for the user, and
        // use the information to validate the password
        $authobj = AuthFactory::create($USER->authinstance);
1097

1098
1099
        // @todo this could be done by a custom form rule... 'password' => $user
        password_validate($form, $values, $USER);
1100

1101
1102
1103
1104
1105
1106
        // The password cannot be the same as the old one
        try {
            if (!$form->get_error('password1')
                && $authobj->authenticate_user_account($USER, $values['password1'])) {
                $form->set_error('password1', get_string('passwordnotchanged'));
            }
1107
        }
1108
1109
1110
        // propagate error up as the collective error AuthUnknownUserException
         catch  (AuthInstanceException $e) {
            $form->set_error('password1', $e->getMessage());
1111
        }
1112
1113
1114
1115
1116

        if ($authobj->authname == 'internal' && isset($values['username']) && $values['username'] != $USER->get('username')) {
            if (!AuthInternal::is_username_valid($values['username'])) {
                $form->set_error('username', get_string('usernameinvalidform', 'auth.internal'));
            }
1117
            if (!$form->get_error('username') && record_exists_select('usr', 'LOWER(username) = ?', array(strtolower($values['username'])))) {
1118
1119
                $form->set_error('username', get_string('usernamealreadytaken', 'auth.internal'));
            }
1120
1121
        }
    }
1122

1123
1124
    // Check if email has been taken
    if (isset($values['email']) && record_exists('artefact_internal_profile_email', 'email', $values['email'])) {
1125
        $form->set_error('email', get_string('unvalidatedemailalreadytaken', 'artefact.internal'));
1126
    }
1127
1128
1129
1130
    // Check if the socialprofile url is valid.
    if (isset($values['socialprofile_hidden']) && $values['socialprofile_hidden'] && $values['socialprofile_profiletype'] == 'webpage' && !filter_var($values['socialprofile_profileurl'], FILTER_VALIDATE_URL)) {
        $form->set_error('socialprofile_profileurl', get_string('notvalidprofileurl', 'artefact.internal'));
    }
1131
1132
}

1133
function requiredfields_submit(Pieform $form, $values) {
1134
    global $USER, $SESSION;
1135

1136
1137
1138
1139
1140
1141
1142
1143
1144
    if (isset($values['password1'])) {
        $authobj = AuthFactory::create($USER->authinstance);

        // This method should exist, because if it did not then the change
        // password form would not have been shown.
        if ($password = $authobj->change_password($USER, $values['password1'])) {
            $SESSION->add_ok_msg(get_string('passwordsaved'));
        }
        else {
1145
            throw new SystemException('Attempt by "' . $USER->get('username') . '@'
1146
1147
1148
                . $USER->get('institution') . 'to change their password failed');
        }
    }
Nigel McNie's avatar
Nigel McNie committed
1149

1150
1151
1152
1153
1154
1155
1156
1157
1158
    if (isset($values['username'])) {
        $SESSION->set('resetusername', false);
        if ($values['username'] != $USER->get('username')) {
            $USER->username = $values['username'];
            $USER->commit();
            $otherfield = true;
        }
    }

1159
1160
1161
1162
1163
1164
    if (isset($values['socialprofile_hidden']) && $values['socialprofile_hidden']) {
        // Socialprofile fields. Save them on their own as they are a fieldset.
        set_profile_field($USER->get('id'), 'socialprofile', $values);
        $otherfield = true;
    }

1165
    foreach ($values as $field => $value) {
1166
1167
1168
        if (in_array($field, array('submit', 'sesskey', 'password1', 'password2', 'username',
                                   'socialprofile_service', 'socialprofile_profiletype',
                                   'socialprofile_profileurl', 'socialprofile_hidden'))) {
1169
1170
1171
            continue;
        }
        if ($field == 'email') {
1172
            $email = $values['email'];
1173
            // Need to update the admin email on installation
1174
            if ($USER->get('admin')) {
1175
1176
1177
1178
1179
                $oldemail = get_field('usr', 'email', 'id', $USER->get('id'));
                if ($oldemail == 'admin@example.org') {
                    // we are dealing with the dummy email that is set on install
                    update_record('usr', array('email' => $email), array('id' => $USER->get('id')));
                    update_record('artefact_internal_profile_email', array('email' => $email), array('owner' => $USER->get('id')));
1180
                    update_record('artefact', array('title' => $email), array('owner' => $USER->get('id'), 'artefacttype' => 'email'));
1181
1182
                }
            }
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199