Search moodle.org's
Developer Documentation

See Release Notes

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

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

   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)
 512                   WHERE e.userid = ? AND c.hostid = ?";
 513  
 514          $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
 515  
 516          $local_courseid_array = 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                  // Pointer to current course:
 527                  $currentcourse =& $currentcourses[$course['remoteid']];
 528                  // We have a record - is it up-to-date?
 529                  $course['id'] = $currentcourse->id;
 530  
 531                  $saveflag = false;
 532  
 533                  foreach($course as $key => $value) {
 534                      if ($currentcourse->$key != $value) {
 535                          $saveflag = true;
 536                          $currentcourse->$key = $value;
 537                      }
 538                  }
 539  
 540                  if ($saveflag) {
 541                      $DB->update_record('mnetervice_enrol_courses', $currentcourse);
 542                  }
 543  
 544                  if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
 545                      $userisregd = true;
 546                  }
 547              } else {
 548                  unset ($courses[$ix]);
 549                  continue;
 550              }
 551  
 552              // By this point, we should always have a $dataObj->id
 553              $local_courseid_array[] = $course['id'];
 554  
 555              // Do we have a record for this assignment?
 556              if ($userisregd) {
 557                  // Yes - we know about this one already
 558                  // We don't want to do updates because the new data is probably
 559                  // 'less complete' than the data we have.
 560              } else {
 561                  // No - create a record
 562                  $assignObj = new stdClass();
 563                  $assignObj->userid    = $userid;
 564                  $assignObj->hostid    = (int)$remoteclient->id;
 565                  $assignObj->remotecourseid = $course['remoteid'];
 566                  $assignObj->rolename  = $course['defaultrolename'];
 567                  $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
 568              }
 569          }
 570  
 571          // Clean up courses that the user is no longer enrolled in.
 572          if (!empty($local_courseid_array)) {
 573              $local_courseid_string = implode(', ', $local_courseid_array);
 574              $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
 575              $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
 576          }
 577      }
 578  
 579      function prevent_local_passwords() {
 580          return true;
 581      }
 582  
 583      /**
 584       * Returns true if this authentication plugin is 'internal'.
 585       *
 586       * @return bool
 587       */
 588      function is_internal() {
 589          return false;
 590      }
 591  
 592      /**
 593       * Returns true if this authentication plugin can change the user's
 594       * password.
 595       *
 596       * @return bool
 597       */
 598      function can_change_password() {
 599          //TODO: it should be able to redirect, right?
 600          return false;
 601      }
 602  
 603      /**
 604       * Returns the URL for changing the user's pw, or false if the default can
 605       * be used.
 606       *
 607       * @return moodle_url
 608       */
 609      function change_password_url() {
 610          return null;
 611      }
 612  
 613      /**
 614       * Poll the IdP server to let it know that a user it has authenticated is still
 615       * online
 616       *
 617       * @return  void
 618       */
 619      function keepalive_client() {
 620          global $CFG, $DB;
 621          $cutoff = time() - 300; // TODO - find out what the remote server's session
 622                                  // cutoff is, and preempt that
 623  
 624          $sql = "
 625              select
 626                  id,
 627                  username,
 628                  mnethostid
 629              from
 630                  {user}
 631              where
 632                  lastaccess > ? AND
 633                  mnethostid != ?
 634              order by
 635                  mnethostid";
 636  
 637          $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
 638  
 639          if ($immigrants == false) {
 640              return true;
 641          }
 642  
 643          $usersArray = array();
 644          foreach($immigrants as $immigrant) {
 645              $usersArray[$immigrant->mnethostid][] = $immigrant->username;
 646          }
 647  
 648          require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
 649          foreach($usersArray as $mnethostid => $users) {
 650              $mnet_peer = new mnet_peer();
 651              $mnet_peer->set_id($mnethostid);
 652  
 653              $mnet_request = new mnet_xmlrpc_client();
 654              $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
 655  
 656              // set $token and $useragent parameters
 657              $mnet_request->add_param($users);
 658  
 659              if ($mnet_request->send($mnet_peer) === true) {
 660                  if (!isset($mnet_request->response['code'])) {
 661                      debugging("Server side error has occured on host $mnethostid");
 662                      continue;
 663                  } elseif ($mnet_request->response['code'] > 0) {
 664                      debugging($mnet_request->response['message']);
 665                  }
 666  
 667                  if (!isset($mnet_request->response['last log id'])) {
 668                      debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
 669                      continue;
 670                  }
 671              } else {
 672                  debugging("Server side error has occured on host $mnethostid: " .
 673                            join("\n", $mnet_request->error));
 674                  break;
 675              }
 676          }
 677      }
 678  
 679      /**
 680       * Receives an array of log entries from an SP and adds them to the mnet_log
 681       * table
 682       *
 683       * @deprecated since Moodle 2.8 Please don't use this function for recording mnet logs.
 684       * @param   array   $array      An array of usernames
 685       * @return  string              "All ok" or an error message
 686       */
 687      function refresh_log($array) {
 688          debugging('refresh_log() is deprecated, The transfer of logs through mnet are no longer recorded.', DEBUG_DEVELOPER);
 689          return array('code' => 0, 'message' => 'All ok');
 690      }
 691  
 692      /**
 693       * Receives an array of usernames from a remote machine and prods their
 694       * sessions to keep them alive
 695       *
 696       * @param   array   $array      An array of usernames
 697       * @return  string              "All ok" or an error message
 698       */
 699      function keepalive_server($array) {
 700          global $CFG, $DB;
 701          $remoteclient = get_mnet_remote_client();
 702  
 703          // We don't want to output anything to the client machine
 704          $start = ob_start();
 705  
 706          // We'll get session records in batches of 30
 707          $superArray = array_chunk($array, 30);
 708  
 709          $returnString = '';
 710  
 711          foreach($superArray as $subArray) {
 712              $subArray = array_values($subArray);
 713              $results = $DB->get_records_list('mnet_session', 'username', $subArray, '', 'id, session_id, username');
 714  
 715              if ($results == false) {
 716                  // We seem to have a username that breaks our query:
 717                  // TODO: Handle this error appropriately
 718                  $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
 719              } else {
 720                  foreach($results as $emigrant) {
 721                      \core\session\manager::touch_session($emigrant->session_id);
 722                  }
 723              }
 724          }
 725  
 726          $end = ob_end_clean();
 727  
 728          if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
 729          return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
 730      }
 731  
 732      /**
 733       * Cleanup any remote mnet_sessions, kill the local mnet_session data
 734       *
 735       * This is called by require_logout in moodlelib
 736       *
 737       * @return   void
 738       */
 739      function prelogout_hook() {
 740          global $CFG, $USER;
 741  
 742          if (!is_enabled_auth('mnet')) {
 743              return;
 744          }
 745  
 746          // If the user is local to this Moodle:
 747          if ($USER->mnethostid == $this->mnet->id) {
 748              $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
 749  
 750          // Else the user has hit 'logout' at a Service Provider Moodle:
 751          } else {
 752              $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
 753  
 754          }
 755      }
 756  
 757      /**
 758       * The SP uses this function to kill the session on the parent IdP
 759       *
 760       * @param   string  $username       Username for session to kill
 761       * @param   string  $useragent      SHA1 hash of user agent to look for
 762       * @return  string                  A plaintext report of what has happened
 763       */
 764      function kill_parent($username, $useragent) {
 765          global $CFG, $USER, $DB;
 766  
 767          require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
 768          $sql = "
 769              select
 770                  *
 771              from
 772                  {mnet_session} s
 773              where
 774                  s.username   = ? AND
 775                  s.useragent  = ? AND
 776                  s.mnethostid = ?";
 777  
 778          $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
 779  
 780          $ignore = $DB->delete_records('mnet_session',
 781                                   array('username'=>$username,
 782                                   'useragent'=>$useragent,
 783                                   'mnethostid'=>$USER->mnethostid));
 784  
 785          if (false != $mnetsessions) {
 786              $mnet_peer = new mnet_peer();
 787              $mnet_peer->set_id($USER->mnethostid);
 788  
 789              $mnet_request = new mnet_xmlrpc_client();
 790              $mnet_request->set_method('auth/mnet/auth.php/kill_children');
 791  
 792              // set $token and $useragent parameters
 793              $mnet_request->add_param($username);
 794              $mnet_request->add_param($useragent);
 795              if ($mnet_request->send($mnet_peer) === false) {
 796                  debugging(join("\n", $mnet_request->error));
 797                  return false;
 798              }
 799          }
 800  
 801          return true;
 802      }
 803  
 804      /**
 805       * The IdP uses this function to kill child sessions on other hosts
 806       *
 807       * @param   string  $username       Username for session to kill
 808       * @param   string  $useragent      SHA1 hash of user agent to look for
 809       * @return  string                  A plaintext report of what has happened
 810       */
 811      function kill_children($username, $useragent) {
 812          global $CFG, $USER, $DB;
 813          $remoteclient = null;
 814          if (defined('MNET_SERVER')) {
 815              $remoteclient = get_mnet_remote_client();
 816          }
 817          require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
 818  
 819          $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
 820  
 821          $returnstring = '';
 822  
 823          $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
 824  
 825          if (false == $mnetsessions) {
 826              $returnstring .= "Could find no remote sessions\n";
 827              $mnetsessions = array();
 828          }
 829  
 830          foreach($mnetsessions as $mnetsession) {
 831              // If this script is being executed by a remote peer, that means the user has clicked
 832              // logout on that peer, and the session on that peer can be deleted natively.
 833              // Skip over it.
 834              if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
 835                  continue;
 836              }
 837              $returnstring .=  "Deleting session\n";
 838  
 839              $mnet_peer = new mnet_peer();
 840              $mnet_peer->set_id($mnetsession->mnethostid);
 841  
 842              $mnet_request = new mnet_xmlrpc_client();
 843              $mnet_request->set_method('auth/mnet/auth.php/kill_child');
 844  
 845              // set $token and $useragent parameters
 846              $mnet_request->add_param($username);
 847              $mnet_request->add_param($useragent);
 848              if ($mnet_request->send($mnet_peer) === false) {
 849                  debugging("Server side error has occured on host $mnetsession->mnethostid: " .
 850                            join("\n", $mnet_request->error));
 851              }
 852          }
 853  
 854          $ignore = $DB->delete_records('mnet_session',
 855                                   array('useragent'=>$useragent, 'userid'=>$userid));
 856  
 857          if (isset($remoteclient) && isset($remoteclient->id)) {
 858              \core\session\manager::kill_user_sessions($userid);
 859          }
 860          return $returnstring;
 861      }
 862  
 863      /**
 864       * When the IdP requests that child sessions are terminated,
 865       * this function will be called on each of the child hosts. The machine that
 866       * calls the function (over xmlrpc) provides us with the mnethostid we need.
 867       *
 868       * @param   string  $username       Username for session to kill
 869       * @param   string  $useragent      SHA1 hash of user agent to look for
 870       * @return  bool                    True on success
 871       */
 872      function kill_child($username, $useragent) {
 873          global $CFG, $DB;
 874          $remoteclient = get_mnet_remote_client();
 875          $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
 876          $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
 877          if (false != $session) {
 878              \core\session\manager::kill_session($session->session_id);
 879              return true;
 880          }
 881          return false;
 882      }
 883  
 884      /**
 885       * To delete a host, we must delete all current sessions that users from
 886       * that host are currently engaged in.
 887       *
 888       * @param   string  $sessionidarray   An array of session hashes
 889       * @return  bool                      True on success
 890       */
 891      function end_local_sessions(&$sessionArray) {
 892          global $CFG;
 893          if (is_array($sessionArray)) {
 894              while($session = array_pop($sessionArray)) {
 895                  \core\session\manager::kill_session($session->session_id);
 896              }
 897              return true;
 898          }
 899          return false;
 900      }
 901  
 902      /**
 903       * Returns the user's profile image info
 904       *
 905       * If the user exists and has a profile picture, the returned array will contain keys:
 906       *  f1          - the content of the default 100x100px image
 907       *  f1_mimetype - the mimetype of the f1 file
 908       *  f2          - the content of the 35x35px variant of the image
 909       *  f2_mimetype - the mimetype of the f2 file
 910       *
 911       * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
 912       *
 913       * @see process_new_icon()
 914       * @uses mnet_remote_client callable via MNet XML-RPC
 915       * @param int $username The id of the user
 916       * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
 917       */
 918      function fetch_user_image($username) {
 919          global $CFG, $DB;
 920  
 921          if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
 922              $fs = get_file_storage();
 923              $usercontext = context_user::instance($user->id, MUST_EXIST);
 924              $return = array();
 925              if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
 926                  $return['f1'] = base64_encode($f1->get_content());
 927                  $return['f1_mimetype'] = $f1->get_mimetype();
 928              } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
 929                  $return['f1'] = base64_encode($f1->get_content());
 930                  $return['f1_mimetype'] = $f1->get_mimetype();
 931              }
 932              if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
 933                  $return['f2'] = base64_encode($f2->get_content());
 934                  $return['f2_mimetype'] = $f2->get_mimetype();
 935              } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
 936                  $return['f2'] = base64_encode($f2->get_content());
 937                  $return['f2_mimetype'] = $f2->get_mimetype();
 938              }
 939              return $return;
 940          }
 941          return false;
 942      }
 943  
 944      /**
 945       * Returns the theme information and logo url as strings.
 946       *
 947       * @return string     The theme info
 948       */
 949      function fetch_theme_info() {
 950          global $CFG;
 951  
 952          $themename = "$CFG->theme";
 953          $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
 954  
 955          $return['themename'] = $themename;
 956          $return['logourl'] = $logourl;
 957          return $return;
 958      }
 959  
 960      /**
 961       * Determines if an MNET host is providing the nominated service.
 962       *
 963       * @param int    $mnethostid   The id of the remote host
 964       * @param string $servicename  The name of the service
 965       * @return bool                Whether the service is available on the remote host
 966       */
 967      function has_service($mnethostid, $servicename) {
 968          global $CFG, $DB;
 969  
 970          $sql = "
 971              SELECT
 972                  svc.id as serviceid,
 973                  svc.name,
 974                  svc.description,
 975                  svc.offer,
 976                  svc.apiversion,
 977                  h2s.id as h2s_id
 978              FROM
 979                  {mnet_host} h,
 980                  {mnet_service} svc,
 981                  {mnet_host2service} h2s
 982              WHERE
 983                  h.deleted = '0' AND
 984                  h.id = h2s.hostid AND
 985                  h2s.hostid = ? AND
 986                  h2s.serviceid = svc.id AND
 987                  svc.name = ? AND
 988                  h2s.subscribe = '1'";
 989  
 990          return $DB->get_records_sql($sql, array($mnethostid, $servicename));
 991      }
 992  
 993      /**
 994       * Checks the MNET access control table to see if the username/mnethost
 995       * is permitted to login to this moodle.
 996       *
 997       * @param string $username   The username
 998       * @param int    $mnethostid The id of the remote mnethost
 999       * @return bool              Whether the user can login from the remote host
1000       */
1001      function can_login_remotely($username, $mnethostid) {
1002          global $DB;
1003  
1004          $accessctrl = 'allow';
1005          $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1006          if (!empty($aclrecord)) {
1007              $accessctrl = $aclrecord->accessctrl;
1008          }
1009          return $accessctrl == 'allow';
1010      }
1011  
1012      function logoutpage_hook() {
1013          global $USER, $CFG, $redirect, $DB;
1014  
1015          if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1016              $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1017              $redirect = $host->wwwroot.'/';
1018          }
1019      }
1020  
1021      /**
1022       * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1023       *
1024       * @param object $logline The log information to be trimmed
1025       * @return object The passed logline object trimmed to not exceed storable limits
1026       */
1027      function trim_logline ($logline) {
1028          $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1029                          'url' => 255);
1030          foreach ($limits as $property => $limit) {
1031              if (isset($logline->$property)) {
1032                  $logline->$property = substr($logline->$property, 0, $limit);
1033              }
1034          }
1035  
1036          return $logline;
1037      }
1038  
1039      /**
1040       * Returns a list of MNet IdPs that the user can roam from.
1041       *
1042       * @param string $wantsurl The relative url fragment the user wants to get to.
1043       * @return array List of arrays with keys url, icon and name.
1044       */
1045      function loginpage_idp_list($wantsurl) {
1046          global $DB, $CFG;
1047  
1048          // strip off wwwroot, since the remote site will prefix it's return url with this
1049          $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . ')/', '', $wantsurl);
1050  
1051          $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1052                    FROM {mnet_host} h
1053                    JOIN {mnet_host2service} m ON h.id = m.hostid
1054                    JOIN {mnet_service} s ON s.id = m.serviceid
1055                    JOIN {mnet_application} a ON h.applicationid = a.id
1056                   WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1057          $params = array('sso_sp', 0, 1);
1058  
1059          if (!empty($CFG->mnet_all_hosts_id)) {
1060              $sql .= " AND h.id <> ?";
1061              $params[] = $CFG->mnet_all_hosts_id;
1062          }
1063  
1064          if (!$hosts = $DB->get_records_sql($sql, $params)) {
1065              return array();
1066          }
1067  
1068          $idps = array();
1069          foreach ($hosts as $host) {
1070              $idps[] = array(
1071                  'url'  => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1072                  'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1073                  'name' => $host->name,
1074              );
1075          }
1076          return $idps;
1077      }
1078  
1079      /**
1080       * Test if settings are correct, print info to output.
1081       */
1082      public function test_settings() {
1083          global $CFG, $OUTPUT, $DB;
1084  
1085          // Generate warning if MNET is disabled.
1086          if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
1087                  echo $OUTPUT->notification(get_string('mnetdisabled', 'mnet'), 'notifyproblem');
1088                  return;
1089          }
1090  
1091          // Generate full list of ID and service providers.
1092          $query = "
1093             SELECT
1094                 h.id,
1095                 h.name as hostname,
1096                 h.wwwroot,
1097                 h2idp.publish as idppublish,
1098                 h2idp.subscribe as idpsubscribe,
1099                 idp.name as idpname,
1100                 h2sp.publish as sppublish,
1101                 h2sp.subscribe as spsubscribe,
1102                 sp.name as spname
1103             FROM
1104                 {mnet_host} h
1105             LEFT JOIN
1106                 {mnet_host2service} h2idp
1107             ON
1108                (h.id = h2idp.hostid AND
1109                (h2idp.publish = 1 OR
1110                 h2idp.subscribe = 1))
1111             INNER JOIN
1112                 {mnet_service} idp
1113             ON
1114                (h2idp.serviceid = idp.id AND
1115                 idp.name = 'sso_idp')
1116             LEFT JOIN
1117                 {mnet_host2service} h2sp
1118             ON
1119                (h.id = h2sp.hostid AND
1120                (h2sp.publish = 1 OR
1121                 h2sp.subscribe = 1))
1122             INNER JOIN
1123                 {mnet_service} sp
1124             ON
1125                (h2sp.serviceid = sp.id AND
1126                 sp.name = 'sso_sp')
1127             WHERE
1128                ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
1129                (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
1130                 h.id != ?
1131             ORDER BY
1132                 h.name ASC";
1133  
1134          $idproviders = array();
1135          $serviceproviders = array();
1136          if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
1137              foreach ($resultset as $hostservice) {
1138                  if (!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
1139                      $serviceproviders[] = array('id' => $hostservice->id,
1140                          'name' => $hostservice->hostname,
1141                          'wwwroot' => $hostservice->wwwroot);
1142                  }
1143                  if (!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
1144                      $idproviders[] = array('id' => $hostservice->id,
1145                          'name' => $hostservice->hostname,
1146                          'wwwroot' => $hostservice->wwwroot);
1147                  }
1148              }
1149          }
1150  
1151          // ID Providers.
1152          $table = html_writer::start_tag('table', array('class' => 'generaltable'));
1153  
1154          $count = 0;
1155          foreach ($idproviders as $host) {
1156              $table .= html_writer::start_tag('tr');
1157              $table .= html_writer::start_tag('td');
1158              $table .= $host['name'];
1159              $table .= html_writer::end_tag('td');
1160              $table .= html_writer::start_tag('td');
1161              $table .= $host['wwwroot'];
1162              $table .= html_writer::end_tag('td');
1163              $table .= html_writer::end_tag('tr');
1164              $count++;
1165          }
1166              $table .= html_writer::end_tag('table');
1167  
1168          if ($count > 0) {
1169              echo html_writer::tag('h3', get_string('auth_mnet_roamin', 'auth_mnet'));
1170              echo $table;
1171          }
1172  
1173          // Service Providers.
1174          unset($table);
1175          $table = html_writer::start_tag('table', array('class' => 'generaltable'));
1176          $count = 0;
1177          foreach ($serviceproviders as $host) {
1178              $table .= html_writer::start_tag('tr');
1179              $table .= html_writer::start_tag('td');
1180              $table .= $host['name'];
1181              $table .= html_writer::end_tag('td');
1182              $table .= html_writer::start_tag('td');
1183              $table .= $host['wwwroot'];
1184              $table .= html_writer::end_tag('td');
1185              $table .= html_writer::end_tag('tr');
1186              $count++;
1187          }
1188              $table .= html_writer::end_tag('table');
1189          if ($count > 0) {
1190              echo html_writer::tag('h3', get_string('auth_mnet_roamout', 'auth_mnet'));
1191              echo $table;
1192          }
1193      }
1194  }