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