See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body