Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/auth/mnet/ -> auth.php (source)

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