Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Authentication Plugin: Moodle Network Authentication
19
 * Multiple host authentication support for Moodle Network.
20
 *
21
 * @package auth_mnet
22
 * @author Martin Dougiamas
23
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
24
 */
25
 
26
defined('MOODLE_INTERNAL') || die();
27
 
28
require_once($CFG->libdir.'/authlib.php');
29
 
30
/**
31
 * Moodle Network authentication plugin.
32
 */
33
class auth_plugin_mnet extends auth_plugin_base {
34
 
35
    /** @var mnet_environment mnet environment. */
36
    protected $mnet;
37
 
38
    /**
39
     * Constructor.
40
     */
41
    public function __construct() {
42
        $this->authtype = 'mnet';
43
        $this->config = get_config('auth_mnet');
44
        $this->mnet = get_mnet_environment();
45
    }
46
 
47
    /**
48
     * Old syntax of class constructor. Deprecated in PHP7.
49
     *
50
     * @deprecated since Moodle 3.1
51
     */
52
    public function auth_plugin_mnet() {
53
        debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
54
        self::__construct();
55
    }
56
 
57
    /**
58
     * This function is normally used to determine if the username and password
59
     * are correct for local logins. Always returns false, as local users do not
60
     * need to login over mnet xmlrpc.
61
     *
62
     * @param string $username The username
63
     * @param string $password The password
64
     * @return bool Authentication success or failure.
65
     */
66
    function user_login($username, $password) {
67
        return false; // Throw moodle_exception("mnetlocal").
68
    }
69
 
70
    /**
71
     * Return user data for the provided token, compare with user_agent string.
72
     *
73
     * @param  string $token    The unique ID provided by remotehost.
74
     * @param  string $useragent       User Agent string.
75
     * @return array  $userdata Array of user info for remote host
76
     */
77
    function user_authorise($token, $useragent) {
78
        global $CFG, $SITE, $DB;
79
        $remoteclient = get_mnet_remote_client();
80
        require_once $CFG->dirroot . '/mnet/xmlrpc/serverlib.php';
81
 
82
        $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent));
83
        if (empty($mnet_session)) {
84
            throw new mnet_server_exception(1, 'authfail_nosessionexists');
85
        }
86
 
87
        // check session confirm timeout
88
        if ($mnet_session->confirm_timeout < time()) {
89
            throw new mnet_server_exception(2, 'authfail_sessiontimedout');
90
        }
91
 
92
        // session okay, try getting the user
93
        if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid))) {
94
            throw new mnet_server_exception(3, 'authfail_usermismatch');
95
        }
96
 
97
        $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient));
98
 
99
        // extra special ones
100
        $userdata['auth']                    = 'mnet';
101
        $userdata['wwwroot']                 = $this->mnet->wwwroot;
102
        $userdata['session.gc_maxlifetime']  = ini_get('session.gc_maxlifetime');
103
 
104
        if (array_key_exists('picture', $userdata) && !empty($user->picture)) {
105
            $fs = get_file_storage();
106
            $usercontext = context_user::instance($user->id, MUST_EXIST);
107
            if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
108
                $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
109
                $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
110
            } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
111
                $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
112
                $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
113
            }
114
        }
115
 
116
        $userdata['myhosts'] = array();
117
        if ($courses = enrol_get_users_courses($user->id, false)) {
118
            $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
119
        }
120
 
121
        $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
122
                       COUNT(c.id) AS count
123
                  FROM {mnetservice_enrol_courses} c
124
                  JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
125
                  JOIN {mnet_host} h ON h.id = c.hostid
126
                 WHERE e.userid = ? AND c.hostid = ?
127
              GROUP BY h.name, h.wwwroot, h.id";
128
 
129
        if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) {
130
            foreach($courses as $course) {
131
                $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
132
            }
133
        }
134
 
135
        return $userdata;
136
    }
137
 
138
    /**
139
     * Generate a random string for use as an RPC session token.
140
     */
141
    function generate_token() {
142
        return sha1(str_shuffle('' . mt_rand() . time()));
143
    }
144
 
145
    /**
146
     * Starts an RPC jump session and returns the jump redirect URL.
147
     *
148
     * @param int $mnethostid id of the mnet host to jump to
149
     * @param string $wantsurl url to redirect to after the jump (usually on remote system)
150
     * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
151
     *                                  rather than somewhere inside *its* wwwroot
152
     */
153
    function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
154
        global $CFG, $USER, $DB;
155
        require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
156
 
157
        if (\core\session\manager::is_loggedinas()) {
158
            throw new \moodle_exception('notpermittedtojumpas', 'mnet');
159
        }
160
 
161
        // check remote login permissions
162
        if (! has_capability('moodle/site:mnetlogintoremote', context_system::instance())
163
                or is_mnet_remote_user($USER)
164
                or isguestuser()
165
                or !isloggedin()) {
166
            throw new \moodle_exception('notpermittedtojump', 'mnet');
167
        }
168
 
169
        // check for SSO publish permission first
170
        if ($this->has_service($mnethostid, 'sso_sp') == false) {
171
            throw new \moodle_exception('hostnotconfiguredforsso', 'mnet');
172
        }
173
 
174
        // set RPC timeout to 30 seconds if not configured
175
        if (empty($this->config->rpc_negotiation_timeout)) {
176
            $this->config->rpc_negotiation_timeout = 30;
177
            set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
178
        }
179
 
180
        // get the host info
181
        $mnet_peer = new mnet_peer();
182
        $mnet_peer->set_id($mnethostid);
183
 
184
        // set up the session
185
        $mnet_session = $DB->get_record('mnet_session',
186
                                   array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
187
                                   'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
188
        if ($mnet_session == false) {
189
            $mnet_session = new stdClass();
190
            $mnet_session->mnethostid = $mnethostid;
191
            $mnet_session->userid = $USER->id;
192
            $mnet_session->username = $USER->username;
193
            $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
194
            $mnet_session->token = $this->generate_token();
195
            $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
196
            $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
197
            $mnet_session->session_id = session_id();
198
            $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
199
        } else {
200
            $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
201
            $mnet_session->token = $this->generate_token();
202
            $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
203
            $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
204
            $mnet_session->session_id = session_id();
205
            $DB->update_record('mnet_session', $mnet_session);
206
        }
207
 
208
        // construct the redirection URL
209
        //$transport = mnet_get_protocol($mnet_peer->transport);
210
        $wantsurl = urlencode($wantsurl);
211
        $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
212
        if ($wantsurlbackhere) {
213
            $url .= '&remoteurl=1';
214
        }
215
 
216
        return $url;
217
    }
218
 
219
    /**
220
     * This function confirms the remote (ID provider) host's mnet session
221
     * by communicating the token and UA over the XMLRPC transport layer, and
222
     * returns the local user record on success.
223
     *
224
     *   @param string    $token           The random session token.
225
     *   @param mnet_peer $remotepeer   The ID provider mnet_peer object.
226
     *   @return array The local user record.
227
     */
228
    function confirm_mnet_session($token, $remotepeer) {
229
        global $CFG, $DB;
230
        require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
231
        require_once $CFG->libdir . '/gdlib.php';
232
        require_once($CFG->dirroot.'/user/lib.php');
233
 
234
        // verify the remote host is configured locally before attempting RPC call
235
        if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
236
            throw new \moodle_exception('notpermittedtoland', 'mnet');
237
        }
238
 
239
        // set up the RPC request
240
        $mnetrequest = new mnet_xmlrpc_client();
241
        $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
242
 
243
        // set $token and $useragent parameters
244
        $mnetrequest->add_param($token);
245
        $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
246
 
247
        // Thunderbirds are go! Do RPC call and store response
248
        if ($mnetrequest->send($remotepeer) === true) {
249
            $remoteuser = (object) $mnetrequest->response;
250
        } else {
251
            foreach ($mnetrequest->error as $errormessage) {
252
                list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
253
                if($code == 702) {
254
                    $site = get_site();
255
                    throw new \moodle_exception('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot,
256
                        format_string($site->fullname));
257
                    exit;
258
                }
259
                $message .= "ERROR $code:<br/>$errormessage<br/>";
260
            }
261
            throw new \moodle_exception("rpcerror", '', '', $message);
262
        }
263
        unset($mnetrequest);
264
 
265
        if (empty($remoteuser) or empty($remoteuser->username)) {
266
            throw new \moodle_exception('unknownerror', 'mnet');
267
            exit;
268
        }
269
 
270
        if (user_not_fully_set_up($remoteuser, false)) {
271
            throw new \moodle_exception('notenoughidpinfo', 'mnet');
272
            exit;
273
        }
274
 
275
        $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
276
 
277
        $remoteuser->auth = 'mnet';
278
        $remoteuser->wwwroot = $remotepeer->wwwroot;
279
 
280
        // the user may roam from Moodle 1.x where lang has _utf8 suffix
281
        // also, make sure that the lang is actually installed, otherwise set site default
282
        if (isset($remoteuser->lang)) {
283
            $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
284
        }
285
 
286
        $firsttime = false;
287
 
288
        // get the local record for the remote user
289
        $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
290
 
291
        // add the remote user to the database if necessary, and if allowed
292
        // TODO: refactor into a separate function
293
        if (empty($localuser) || ! $localuser->id) {
294
            /*
295
            if (empty($this->config->auto_add_remote_users)) {
296
                throw new \moodle_exception('nolocaluser', 'mnet');
297
            } See MDL-21327   for why this is commented out
298
            */
299
            $remoteuser->mnethostid = $remotehost->id;
300
            $remoteuser->firstaccess = 0;
301
            $remoteuser->confirmed = 1;
302
 
303
            $remoteuser->id = user_create_user($remoteuser, false);
304
            $firsttime = true;
305
            $localuser = $remoteuser;
306
        }
307
 
308
        // check sso access control list for permission first
309
        if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
310
            throw new \moodle_exception('sso_mnet_login_refused', 'mnet', '',
311
                array('user' => $localuser->username, 'host' => $remotehost->name));
312
        }
313
 
314
        $fs = get_file_storage();
315
 
316
        // update the local user record with remote user data
317
        foreach ((array) $remoteuser as $key => $val) {
318
 
319
            if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
320
                // update the user picture if there is a newer verion at the identity provider
321
                $usercontext = context_user::instance($localuser->id, MUST_EXIST);
322
                if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
323
                    $localtimemodified = $usericonfile->get_timemodified();
324
                } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
325
                    $localtimemodified = $usericonfile->get_timemodified();
326
                } else {
327
                    $localtimemodified = 0;
328
                }
329
 
330
                if (!empty($val) and $localtimemodified < $val) {
331
                    mnet_debug('refetching the user picture from the identity provider host');
332
                    $fetchrequest = new mnet_xmlrpc_client();
333
                    $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
334
                    $fetchrequest->add_param($localuser->username);
335
                    if ($fetchrequest->send($remotepeer) === true) {
336
                        if (strlen($fetchrequest->response['f1']) > 0) {
337
                            $imagefilename = $CFG->tempdir . '/mnet-usericon-' . $localuser->id;
338
                            $imagecontents = base64_decode($fetchrequest->response['f1']);
339
                            file_put_contents($imagefilename, $imagecontents);
340
                            if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
341
                                $localuser->picture = $newrev;
342
                            }
343
                            unlink($imagefilename);
344
                        }
345
                        // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
346
                        // the mimetype information provided is ignored and the type of the file is detected
347
                        // by process_new_icon()
348
                    }
349
                }
350
            }
351
 
352
            if($key == 'myhosts') {
353
                $localuser->mnet_foreign_host_array = array();
354
                foreach($val as $rhost) {
355
                    $name  = clean_param($rhost['name'], PARAM_ALPHANUM);
356
                    $url   = clean_param($rhost['url'], PARAM_URL);
357
                    $count = clean_param($rhost['count'], PARAM_INT);
358
                    $url_is_local = stristr($url , $CFG->wwwroot);
359
                    if (!empty($name) && !empty($count) && empty($url_is_local)) {
360
                        $localuser->mnet_foreign_host_array[] = array('name'  => $name,
361
                                                                      'url'   => $url,
362
                                                                      'count' => $count);
363
                    }
364
                }
365
            }
366
 
367
            $localuser->{$key} = $val;
368
        }
369
 
370
        $localuser->mnethostid = $remotepeer->id;
371
        user_update_user($localuser, false);
372
 
373
        if (!$firsttime) {
374
            // repeat customer! let the IDP know about enrolments
375
            // we have for this user.
376
            // set up the RPC request
377
            $mnetrequest = new mnet_xmlrpc_client();
378
            $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
379
 
380
            // pass username and an assoc array of "my courses"
381
            // with info so that the IDP can maintain mnetservice_enrol_enrolments
382
            $mnetrequest->add_param($remoteuser->username);
383
            $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
384
            $courses = enrol_get_users_courses($localuser->id, false, $fields);
385
            if (is_array($courses) && !empty($courses)) {
386
                // Second request to do the JOINs that we'd have done
387
                // inside enrol_get_users_courses() if we had been allowed
388
                $sql = "SELECT c.id,
389
                               cc.name AS cat_name, cc.description AS cat_description
390
                          FROM {course} c
391
                          JOIN {course_categories} cc ON c.category = cc.id
392
                         WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
393
                $extra = $DB->get_records_sql($sql);
394
 
395
                $keys = array_keys($courses);
396
                $studentroles = get_archetype_roles('student');
397
                if (!empty($studentroles)) {
398
                    $defaultrole = reset($studentroles);
399
                    //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
400
                    foreach ($keys AS $id) {
401
                        if ($courses[$id]->visible == 0) {
402
                            unset($courses[$id]);
403
                            continue;
404
                        }
405
                        $courses[$id]->cat_id          = $courses[$id]->category;
406
                        $courses[$id]->defaultroleid   = $defaultrole->id;
407
                        unset($courses[$id]->category);
408
                        unset($courses[$id]->visible);
409
 
410
                        $courses[$id]->cat_name        = $extra[$id]->cat_name;
411
                        $courses[$id]->cat_description = $extra[$id]->cat_description;
412
                        $courses[$id]->defaultrolename = $defaultrole->name;
413
                        // coerce to array
414
                        $courses[$id] = (array)$courses[$id];
415
                    }
416
                } else {
417
                    throw new moodle_exception('unknownrole', 'error', '', 'student');
418
                }
419
            } else {
420
                // if the array is empty, send it anyway
421
                // we may be clearing out stale entries
422
                $courses = array();
423
            }
424
            $mnetrequest->add_param($courses, 'array');
425
 
426
            // Call 0800-RPC Now! -- we don't care too much if it fails
427
            // as it's just informational.
428
            if ($mnetrequest->send($remotepeer) === false) {
429
                // error_log(print_r($mnetrequest->error,1));
430
            }
431
        }
432
 
433
        return $localuser;
434
    }
435
 
436
 
437
    /**
438
     * creates (or updates) the mnet session once
439
     * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
440
     *
441
     * @param stdclass  $user the local user (must exist already
442
     * @param string    $token the jump/land token
443
     * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
444
     */
445
    public function update_mnet_session($user, $token, $remotepeer) {
446
        global $DB;
447
        $session_gc_maxlifetime = 1440;
448
        if (isset($user->session_gc_maxlifetime)) {
449
            $session_gc_maxlifetime = $user->session_gc_maxlifetime;
450
        }
451
        if (!$mnet_session = $DB->get_record('mnet_session',
452
                                   array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
453
                                   'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
454
            $mnet_session = new stdClass();
455
            $mnet_session->mnethostid = $remotepeer->id;
456
            $mnet_session->userid = $user->id;
457
            $mnet_session->username = $user->username;
458
            $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
459
            $mnet_session->token = $token; // Needed to support simultaneous sessions
460
                                           // and preserving DB rec uniqueness
461
            $mnet_session->confirm_timeout = time();
462
            $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
463
            $mnet_session->session_id = session_id();
464
            $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
465
        } else {
466
            $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
467
            $DB->update_record('mnet_session', $mnet_session);
468
        }
469
    }
470
 
471
 
472
 
473
    /**
474
     * Invoke this function _on_ the IDP to update it with enrolment info local to
475
     * the SP right after calling user_authorise()
476
     *
477
     * Normally called by the SP after calling user_authorise()
478
     *
479
     * @param string $username The username
480
     * @param array $courses  Assoc array of courses following the structure of mnetservice_enrol_courses
481
     * @return bool
482
     */
483
    function update_enrolments($username, $courses) {
484
        global $CFG, $DB;
485
        $remoteclient = get_mnet_remote_client();
486
 
487
        if (empty($username) || !is_array($courses)) {
488
            return false;
489
        }
490
        // make sure it is a user we have an in active session
491
        // with that host...
492
        $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
493
        $userid = null;
494
        foreach ($mnetsessions as $mnetsession) {
495
            if (is_null($userid)) {
496
                $userid = $mnetsession->userid;
497
                continue;
498
            }
499
            if ($userid != $mnetsession->userid) {
500
                throw new mnet_server_exception(3, 'authfail_usermismatch');
501
            }
502
        }
503
 
504
        if (empty($courses)) { // no courses? clear out quickly
505
            $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
506
            return true;
507
        }
508
 
509
        // IMPORTANT: Ask for remoteid as the first element in the query, so
510
        // that the array that comes back is indexed on the same field as the
511
        // array that we have received from the remote client
512
        $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
513
                       c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
514
                       e.id AS enrolmentid
515
                  FROM {mnetservice_enrol_courses} c
516
             LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid AND e.userid = ?)
517
                 WHERE c.hostid = ?";
518
 
519
        $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
520
 
521
        $keepenrolments = array();
522
        foreach($courses as $ix => $course) {
523
 
524
            $course['remoteid'] = $course['id'];
525
            $course['hostid']   =  (int)$remoteclient->id;
526
            $userisregd         = false;
527
 
528
            // if we do not have the the information about the remote course, it is not available
529
            // to us for remote enrolment - skip
530
            if (array_key_exists($course['remoteid'], $currentcourses)) {
531
                // We are going to keep this enrolment, it will be updated or inserted, but will keep it.
532
                $keepenrolments[] = $course['id'];
533
 
534
                // Pointer to current course:
535
                $currentcourse =& $currentcourses[$course['remoteid']];
536
 
537
                $saveflag = false;
538
 
539
                foreach($course as $key => $value) {
540
                    // Only compare what is available locally, data coming from enrolment tables have
541
                    // way more information that tables used to keep the track of mnet enrolments.
542
                    if (!property_exists($currentcourse, $key)) {
543
                        continue;
544
                    }
545
                    // Don't compare ids either, they come from different databases.
546
                    if ($key === 'id') {
547
                        continue;
548
                    }
549
 
550
                    if ($currentcourse->$key != $value) {
551
                        $saveflag = true;
552
                        $currentcourse->$key = $value;
553
                    }
554
                }
555
 
556
                if ($saveflag) {
557
                    $DB->update_record('mnetservice_enrol_courses', $currentcourse);
558
                }
559
 
560
                if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
561
                    $userisregd = true;
562
                }
563
            } else {
564
                unset ($courses[$ix]);
565
                continue;
566
            }
567
 
568
            // Do we have a record for this assignment?
569
            if ($userisregd) {
570
                // Yes - we know about this one already
571
                // We don't want to do updates because the new data is probably
572
                // 'less complete' than the data we have.
573
            } else {
574
                // No - create a record
575
                $newenrol = new stdClass();
576
                $newenrol->userid    = $userid;
577
                $newenrol->hostid    = (int)$remoteclient->id;
578
                $newenrol->remotecourseid = $course['remoteid'];
579
                $newenrol->rolename  = $course['defaultrolename'];
580
                $newenrol->enroltype = 'mnet';
581
                $newenrol->id = $DB->insert_record('mnetservice_enrol_enrolments', $newenrol);
582
            }
583
        }
584
 
585
        // Clean up courses that the user is no longer enrolled in.
586
        list($insql, $inparams) = $DB->get_in_or_equal($keepenrolments, SQL_PARAMS_NAMED, 'param', false, null);
587
        $whereclause = ' userid = :userid AND hostid = :hostid AND remotecourseid ' . $insql;
588
        $params = array_merge(['userid' => $userid, 'hostid' => $remoteclient->id], $inparams);
589
        $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, $params);
590
    }
591
 
592
    function prevent_local_passwords() {
593
        return true;
594
    }
595
 
596
    /**
597
     * Returns true if this authentication plugin is 'internal'.
598
     *
599
     * @return bool
600
     */
601
    function is_internal() {
602
        return false;
603
    }
604
 
605
    /**
606
     * Returns true if this authentication plugin can change the user's
607
     * password.
608
     *
609
     * @return bool
610
     */
611
    function can_change_password() {
612
        //TODO: it should be able to redirect, right?
613
        return false;
614
    }
615
 
616
    /**
617
     * Returns the URL for changing the user's pw, or false if the default can
618
     * be used.
619
     *
620
     * @return moodle_url
621
     */
622
    function change_password_url() {
623
        return null;
624
    }
625
 
626
    /**
627
     * Poll the IdP server to let it know that a user it has authenticated is still
628
     * online
629
     *
630
     * @return  void
631
     */
632
    function keepalive_client() {
633
        global $CFG, $DB;
634
        $cutoff = time() - 300; // TODO - find out what the remote server's session
635
                                // cutoff is, and preempt that
636
 
637
        $sql = "
638
            select
639
                id,
640
                username,
641
                mnethostid
642
            from
643
                {user}
644
            where
645
                lastaccess > ? AND
646
                mnethostid != ?
647
            order by
648
                mnethostid";
649
 
650
        $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
651
 
652
        if ($immigrants == false) {
653
            return true;
654
        }
655
 
656
        $usersArray = array();
657
        foreach($immigrants as $immigrant) {
658
            $usersArray[$immigrant->mnethostid][] = $immigrant->username;
659
        }
660
 
661
        require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
662
        foreach($usersArray as $mnethostid => $users) {
663
            $mnet_peer = new mnet_peer();
664
            $mnet_peer->set_id($mnethostid);
665
 
666
            $mnet_request = new mnet_xmlrpc_client();
667
            $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
668
 
669
            // set $token and $useragent parameters
670
            $mnet_request->add_param($users);
671
 
672
            if ($mnet_request->send($mnet_peer) === true) {
673
                if (!isset($mnet_request->response['code'])) {
674
                    debugging("Server side error has occured on host $mnethostid");
675
                    continue;
676
                } elseif ($mnet_request->response['code'] > 0) {
677
                    debugging($mnet_request->response['message']);
678
                }
679
 
680
                if (!isset($mnet_request->response['last log id'])) {
681
                    debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
682
                    continue;
683
                }
684
            } else {
685
                debugging("Server side error has occured on host $mnethostid: " .
686
                          join("\n", $mnet_request->error));
687
                break;
688
            }
689
        }
690
    }
691
 
692
    /**
693
     * Receives an array of log entries from an SP and adds them to the mnet_log
694
     * table
695
     *
696
     * @deprecated since Moodle 2.8 Please don't use this function for recording mnet logs.
697
     * @param   array   $array      An array of usernames
698
     * @return  string              "All ok" or an error message
699
     */
700
    function refresh_log($array) {
701
        debugging('refresh_log() is deprecated, The transfer of logs through mnet are no longer recorded.', DEBUG_DEVELOPER);
702
        return array('code' => 0, 'message' => 'All ok');
703
    }
704
 
705
    /**
706
     * Receives an array of usernames from a remote machine and prods their
707
     * sessions to keep them alive
708
     *
709
     * @param   array   $array      An array of usernames
710
     * @return  string              "All ok" or an error message
711
     */
712
    function keepalive_server($array) {
713
        global $CFG, $DB;
714
        $remoteclient = get_mnet_remote_client();
715
 
716
        // We don't want to output anything to the client machine
717
        $start = ob_start();
718
 
719
        // We'll get session records in batches of 30
720
        $superArray = array_chunk($array, 30);
721
 
722
        $returnString = '';
723
 
724
        foreach($superArray as $subArray) {
725
            $subArray = array_values($subArray);
726
            $results = $DB->get_records_list('mnet_session', 'username', $subArray, '', 'id, session_id, username');
727
 
728
            if ($results == false) {
729
                // We seem to have a username that breaks our query:
730
                // TODO: Handle this error appropriately
731
                $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
732
            } else {
733
                foreach($results as $emigrant) {
734
                    \core\session\manager::touch_session($emigrant->session_id);
735
                }
736
            }
737
        }
738
 
739
        $end = ob_end_clean();
740
 
741
        if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
742
        return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
743
    }
744
 
745
    /**
746
     * Cleanup any remote mnet_sessions, kill the local mnet_session data
747
     *
748
     * This is called by require_logout in moodlelib
749
     *
750
     * @return   void
751
     */
752
    function prelogout_hook() {
753
        global $CFG, $USER;
754
 
755
        if (!is_enabled_auth('mnet')) {
756
            return;
757
        }
758
 
759
        // If the user is local to this Moodle:
760
        if ($USER->mnethostid == $this->mnet->id) {
761
            $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
762
 
763
        // Else the user has hit 'logout' at a Service Provider Moodle:
764
        } else {
765
            $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
766
 
767
        }
768
    }
769
 
770
    /**
771
     * The SP uses this function to kill the session on the parent IdP
772
     *
773
     * @param   string  $username       Username for session to kill
774
     * @param   string  $useragent      SHA1 hash of user agent to look for
775
     * @return  string                  A plaintext report of what has happened
776
     */
777
    function kill_parent($username, $useragent) {
778
        global $CFG, $USER, $DB;
779
 
780
        require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
781
        $sql = "
782
            select
783
                *
784
            from
785
                {mnet_session} s
786
            where
787
                s.username   = ? AND
788
                s.useragent  = ? AND
789
                s.mnethostid = ?";
790
 
791
        $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
792
 
793
        $ignore = $DB->delete_records('mnet_session',
794
                                 array('username'=>$username,
795
                                 'useragent'=>$useragent,
796
                                 'mnethostid'=>$USER->mnethostid));
797
 
798
        if (false != $mnetsessions) {
799
            $mnet_peer = new mnet_peer();
800
            $mnet_peer->set_id($USER->mnethostid);
801
 
802
            $mnet_request = new mnet_xmlrpc_client();
803
            $mnet_request->set_method('auth/mnet/auth.php/kill_children');
804
 
805
            // set $token and $useragent parameters
806
            $mnet_request->add_param($username);
807
            $mnet_request->add_param($useragent);
808
            if ($mnet_request->send($mnet_peer) === false) {
809
                debugging(join("\n", $mnet_request->error));
810
                return false;
811
            }
812
        }
813
 
814
        return true;
815
    }
816
 
817
    /**
818
     * The IdP uses this function to kill child sessions on other hosts
819
     *
820
     * @param   string  $username       Username for session to kill
821
     * @param   string  $useragent      SHA1 hash of user agent to look for
822
     * @return  string                  A plaintext report of what has happened
823
     */
824
    function kill_children($username, $useragent) {
825
        global $CFG, $USER, $DB;
826
        $remoteclient = null;
827
        if (defined('MNET_SERVER')) {
828
            $remoteclient = get_mnet_remote_client();
829
        }
830
        require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
831
 
832
        $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
833
 
834
        $returnstring = '';
835
 
836
        $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
837
 
838
        if (false == $mnetsessions) {
839
            $returnstring .= "Could find no remote sessions\n";
840
            $mnetsessions = array();
841
        }
842
 
843
        foreach($mnetsessions as $mnetsession) {
844
            // If this script is being executed by a remote peer, that means the user has clicked
845
            // logout on that peer, and the session on that peer can be deleted natively.
846
            // Skip over it.
847
            if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
848
                continue;
849
            }
850
            $returnstring .=  "Deleting session\n";
851
 
852
            $mnet_peer = new mnet_peer();
853
            $mnet_peer->set_id($mnetsession->mnethostid);
854
 
855
            $mnet_request = new mnet_xmlrpc_client();
856
            $mnet_request->set_method('auth/mnet/auth.php/kill_child');
857
 
858
            // set $token and $useragent parameters
859
            $mnet_request->add_param($username);
860
            $mnet_request->add_param($useragent);
861
            if ($mnet_request->send($mnet_peer) === false) {
862
                debugging("Server side error has occured on host $mnetsession->mnethostid: " .
863
                          join("\n", $mnet_request->error));
864
            }
865
        }
866
 
867
        $ignore = $DB->delete_records('mnet_session',
868
                                 array('useragent'=>$useragent, 'userid'=>$userid));
869
 
870
        if (isset($remoteclient) && isset($remoteclient->id)) {
871
            \core\session\manager::kill_user_sessions($userid);
872
        }
873
        return $returnstring;
874
    }
875
 
876
    /**
877
     * When the IdP requests that child sessions are terminated,
878
     * this function will be called on each of the child hosts. The machine that
879
     * calls the function (over xmlrpc) provides us with the mnethostid we need.
880
     *
881
     * @param   string  $username       Username for session to kill
882
     * @param   string  $useragent      SHA1 hash of user agent to look for
883
     * @return  bool                    True on success
884
     */
885
    function kill_child($username, $useragent) {
886
        global $CFG, $DB;
887
        $remoteclient = get_mnet_remote_client();
888
        $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
889
        $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
890
        if (false != $session) {
891
            \core\session\manager::kill_session($session->session_id);
892
            return true;
893
        }
894
        return false;
895
    }
896
 
897
    /**
898
     * To delete a host, we must delete all current sessions that users from
899
     * that host are currently engaged in.
900
     *
901
     * @param   string  $sessionidarray   An array of session hashes
902
     * @return  bool                      True on success
903
     */
904
    function end_local_sessions(&$sessionArray) {
905
        global $CFG;
906
        if (is_array($sessionArray)) {
907
            while($session = array_pop($sessionArray)) {
908
                \core\session\manager::kill_session($session->session_id);
909
            }
910
            return true;
911
        }
912
        return false;
913
    }
914
 
915
    /**
916
     * Returns the user's profile image info
917
     *
918
     * If the user exists and has a profile picture, the returned array will contain keys:
919
     *  f1          - the content of the default 100x100px image
920
     *  f1_mimetype - the mimetype of the f1 file
921
     *  f2          - the content of the 35x35px variant of the image
922
     *  f2_mimetype - the mimetype of the f2 file
923
     *
924
     * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
925
     *
926
     * @see process_new_icon()
927
     * @uses mnet_remote_client callable via MNet XML-RPC
928
     * @param int $username The id of the user
929
     * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
930
     */
931
    function fetch_user_image($username) {
932
        global $CFG, $DB;
933
 
934
        if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
935
            $fs = get_file_storage();
936
            $usercontext = context_user::instance($user->id, MUST_EXIST);
937
            $return = array();
938
            if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
939
                $return['f1'] = base64_encode($f1->get_content());
940
                $return['f1_mimetype'] = $f1->get_mimetype();
941
            } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
942
                $return['f1'] = base64_encode($f1->get_content());
943
                $return['f1_mimetype'] = $f1->get_mimetype();
944
            }
945
            if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
946
                $return['f2'] = base64_encode($f2->get_content());
947
                $return['f2_mimetype'] = $f2->get_mimetype();
948
            } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
949
                $return['f2'] = base64_encode($f2->get_content());
950
                $return['f2_mimetype'] = $f2->get_mimetype();
951
            }
952
            return $return;
953
        }
954
        return false;
955
    }
956
 
957
    /**
958
     * Returns the theme information and logo url as strings.
959
     *
960
     * @return string     The theme info
961
     */
962
    function fetch_theme_info() {
963
        global $CFG;
964
 
965
        $themename = "$CFG->theme";
966
        $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
967
 
968
        $return['themename'] = $themename;
969
        $return['logourl'] = $logourl;
970
        return $return;
971
    }
972
 
973
    /**
974
     * Determines if an MNET host is providing the nominated service.
975
     *
976
     * @param int    $mnethostid   The id of the remote host
977
     * @param string $servicename  The name of the service
978
     * @return bool                Whether the service is available on the remote host
979
     */
980
    function has_service($mnethostid, $servicename) {
981
        global $CFG, $DB;
982
 
983
        $sql = "
984
            SELECT
985
                svc.id as serviceid,
986
                svc.name,
987
                svc.description,
988
                svc.offer,
989
                svc.apiversion,
990
                h2s.id as h2s_id
991
            FROM
992
                {mnet_host} h,
993
                {mnet_service} svc,
994
                {mnet_host2service} h2s
995
            WHERE
996
                h.deleted = '0' AND
997
                h.id = h2s.hostid AND
998
                h2s.hostid = ? AND
999
                h2s.serviceid = svc.id AND
1000
                svc.name = ? AND
1001
                h2s.subscribe = '1'";
1002
 
1003
        return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1004
    }
1005
 
1006
    /**
1007
     * Checks the MNET access control table to see if the username/mnethost
1008
     * is permitted to login to this moodle.
1009
     *
1010
     * @param string $username   The username
1011
     * @param int    $mnethostid The id of the remote mnethost
1012
     * @return bool              Whether the user can login from the remote host
1013
     */
1014
    function can_login_remotely($username, $mnethostid) {
1015
        global $DB;
1016
 
1017
        $accessctrl = 'allow';
1018
        $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1019
        if (!empty($aclrecord)) {
1020
            $accessctrl = $aclrecord->accessctrl;
1021
        }
1022
        return $accessctrl == 'allow';
1023
    }
1024
 
1025
    function logoutpage_hook() {
1026
        global $USER, $CFG, $redirect, $DB;
1027
 
1028
        if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1029
            $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1030
            $redirect = $host->wwwroot.'/';
1031
        }
1032
    }
1033
 
1034
    /**
1035
     * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1036
     *
1037
     * @param object $logline The log information to be trimmed
1038
     * @return object The passed logline object trimmed to not exceed storable limits
1039
     */
1040
    function trim_logline($logline) {
1041
        $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1042
                        'url' => 255);
1043
        foreach ($limits as $property => $limit) {
1044
            if (isset($logline->$property)) {
1045
                $logline->$property = substr($logline->$property, 0, $limit);
1046
            }
1047
        }
1048
 
1049
        return $logline;
1050
    }
1051
 
1052
    /**
1053
     * Returns a list of MNet IdPs that the user can roam from.
1054
     *
1055
     * @param string $wantsurl The relative url fragment the user wants to get to.
1056
     * @return array List of arrays with keys url, icon and name.
1057
     */
1058
    function loginpage_idp_list($wantsurl) {
1059
        global $DB, $CFG;
1060
 
1061
        // strip off wwwroot, since the remote site will prefix it's return url with this
1062
        $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . ')/', '', $wantsurl);
1063
 
1064
        $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1065
                  FROM {mnet_host} h
1066
                  JOIN {mnet_host2service} m ON h.id = m.hostid
1067
                  JOIN {mnet_service} s ON s.id = m.serviceid
1068
                  JOIN {mnet_application} a ON h.applicationid = a.id
1069
                 WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1070
        $params = array('sso_sp', 0, 1);
1071
 
1072
        if (!empty($CFG->mnet_all_hosts_id)) {
1073
            $sql .= " AND h.id <> ?";
1074
            $params[] = $CFG->mnet_all_hosts_id;
1075
        }
1076
 
1077
        if (!$hosts = $DB->get_records_sql($sql, $params)) {
1078
            return array();
1079
        }
1080
 
1081
        $idps = array();
1082
        foreach ($hosts as $host) {
1083
            $idps[] = array(
1084
                'url'  => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1085
                'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1086
                'name' => $host->name,
1087
            );
1088
        }
1089
        return $idps;
1090
    }
1091
 
1092
    /**
1093
     * Test if settings are correct, print info to output.
1094
     */
1095
    public function test_settings() {
1096
        global $CFG, $OUTPUT, $DB;
1097
 
1098
        // Generate warning if MNET is disabled.
1099
        if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
1100
                echo $OUTPUT->notification(get_string('mnetdisabled', 'mnet'), 'notifyproblem');
1101
                return;
1102
        }
1103
 
1104
        // Generate full list of ID and service providers.
1105
        $query = "
1106
           SELECT
1107
               h.id,
1108
               h.name as hostname,
1109
               h.wwwroot,
1110
               h2idp.publish as idppublish,
1111
               h2idp.subscribe as idpsubscribe,
1112
               idp.name as idpname,
1113
               h2sp.publish as sppublish,
1114
               h2sp.subscribe as spsubscribe,
1115
               sp.name as spname
1116
           FROM
1117
               {mnet_host} h
1118
           LEFT JOIN
1119
               {mnet_host2service} h2idp
1120
           ON
1121
              (h.id = h2idp.hostid AND
1122
              (h2idp.publish = 1 OR
1123
               h2idp.subscribe = 1))
1124
           INNER JOIN
1125
               {mnet_service} idp
1126
           ON
1127
              (h2idp.serviceid = idp.id AND
1128
               idp.name = 'sso_idp')
1129
           LEFT JOIN
1130
               {mnet_host2service} h2sp
1131
           ON
1132
              (h.id = h2sp.hostid AND
1133
              (h2sp.publish = 1 OR
1134
               h2sp.subscribe = 1))
1135
           INNER JOIN
1136
               {mnet_service} sp
1137
           ON
1138
              (h2sp.serviceid = sp.id AND
1139
               sp.name = 'sso_sp')
1140
           WHERE
1141
              ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
1142
              (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
1143
               h.id != ?
1144
           ORDER BY
1145
               h.name ASC";
1146
 
1147
        $idproviders = array();
1148
        $serviceproviders = array();
1149
        if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
1150
            foreach ($resultset as $hostservice) {
1151
                if (!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
1152
                    $serviceproviders[] = array('id' => $hostservice->id,
1153
                        'name' => $hostservice->hostname,
1154
                        'wwwroot' => $hostservice->wwwroot);
1155
                }
1156
                if (!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
1157
                    $idproviders[] = array('id' => $hostservice->id,
1158
                        'name' => $hostservice->hostname,
1159
                        'wwwroot' => $hostservice->wwwroot);
1160
                }
1161
            }
1162
        }
1163
 
1164
        // ID Providers.
1165
        $table = html_writer::start_tag('table', array('class' => 'generaltable'));
1166
 
1167
        $count = 0;
1168
        foreach ($idproviders as $host) {
1169
            $table .= html_writer::start_tag('tr');
1170
            $table .= html_writer::start_tag('td');
1171
            $table .= $host['name'];
1172
            $table .= html_writer::end_tag('td');
1173
            $table .= html_writer::start_tag('td');
1174
            $table .= $host['wwwroot'];
1175
            $table .= html_writer::end_tag('td');
1176
            $table .= html_writer::end_tag('tr');
1177
            $count++;
1178
        }
1179
            $table .= html_writer::end_tag('table');
1180
 
1181
        if ($count > 0) {
1182
            echo html_writer::tag('h3', get_string('auth_mnet_roamin', 'auth_mnet'));
1183
            echo $table;
1184
        }
1185
 
1186
        // Service Providers.
1187
        unset($table);
1188
        $table = html_writer::start_tag('table', array('class' => 'generaltable'));
1189
        $count = 0;
1190
        foreach ($serviceproviders as $host) {
1191
            $table .= html_writer::start_tag('tr');
1192
            $table .= html_writer::start_tag('td');
1193
            $table .= $host['name'];
1194
            $table .= html_writer::end_tag('td');
1195
            $table .= html_writer::start_tag('td');
1196
            $table .= $host['wwwroot'];
1197
            $table .= html_writer::end_tag('td');
1198
            $table .= html_writer::end_tag('tr');
1199
            $count++;
1200
        }
1201
            $table .= html_writer::end_tag('table');
1202
        if ($count > 0) {
1203
            echo html_writer::tag('h3', get_string('auth_mnet_roamout', 'auth_mnet'));
1204
            echo $table;
1205
        }
1206
    }
1207
}