[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/auth/mnet/ -> auth.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1