Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/auth/mnet/ -> auth.php (source)

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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