Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
/auth/mnet/ -> auth.php (source)

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

   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  }