Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
/auth/mnet/ -> auth.php (source)

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