2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Authentication Plugin: Moodle Network Authentication
19 * Multiple host authentication support for Moodle Network.
22 * @author Martin Dougiamas
23 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
26 defined('MOODLE_INTERNAL') ||
die();
28 require_once($CFG->libdir
.'/authlib.php');
31 * Moodle Network authentication plugin.
33 class auth_plugin_mnet
extends auth_plugin_base
{
38 public function __construct() {
39 $this->authtype
= 'mnet';
40 $this->config
= get_config('auth_mnet');
41 $this->mnet
= get_mnet_environment();
45 * Old syntax of class constructor. Deprecated in PHP7.
47 * @deprecated since Moodle 3.1
49 public function auth_plugin_mnet() {
50 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER
);
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.
59 * @param string $username The username
60 * @param string $password The password
61 * @return bool Authentication success or failure.
63 function user_login($username, $password) {
64 return false; // print_error("mnetlocal");
68 * Return user data for the provided token, compare with user_agent string.
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
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';
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');
84 // check session confirm timeout
85 if ($mnet_session->confirm_timeout
< time()) {
86 throw new mnet_server_exception(2, 'authfail_sessiontimedout');
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');
94 $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient));
97 $userdata['auth'] = 'mnet';
98 $userdata['wwwroot'] = $this->mnet
->wwwroot
;
99 $userdata['session.gc_maxlifetime'] = ini_get('session.gc_maxlifetime');
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();
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));
118 $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
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";
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
);
136 * Generate a random string for use as an RPC session token.
138 function generate_token() {
139 return sha1(str_shuffle('' . mt_rand() . time()));
143 * Starts an RPC jump session and returns the jump redirect URL.
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
150 function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
151 global $CFG, $USER, $DB;
152 require_once $CFG->dirroot
. '/mnet/xmlrpc/client.php';
154 if (\core\session\manager
::is_loggedinas()) {
155 print_error('notpermittedtojumpas', 'mnet');
158 // check remote login permissions
159 if (! has_capability('moodle/site:mnetlogintoremote', context_system
::instance())
160 or is_mnet_remote_user($USER)
163 print_error('notpermittedtojump', 'mnet');
166 // check for SSO publish permission first
167 if ($this->has_service($mnethostid, 'sso_sp') == false) {
168 print_error('hostnotconfiguredforsso', 'mnet');
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');
178 $mnet_peer = new mnet_peer();
179 $mnet_peer->set_id($mnethostid);
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);
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);
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';
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.
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.
225 function confirm_mnet_session($token, $remotepeer) {
227 require_once $CFG->dirroot
. '/mnet/xmlrpc/client.php';
228 require_once $CFG->libdir
. '/gdlib.php';
229 require_once($CFG->dirroot
.'/user/lib.php');
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');
236 // set up the RPC request
237 $mnetrequest = new mnet_xmlrpc_client();
238 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
240 // set $token and $useragent parameters
241 $mnetrequest->add_param($token);
242 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
244 // Thunderbirds are go! Do RPC call and store response
245 if ($mnetrequest->send($remotepeer) === true) {
246 $remoteuser = (object) $mnetrequest->response
;
248 foreach ($mnetrequest->error
as $errormessage) {
249 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
252 print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot
, format_string($site->fullname
));
255 $message .= "ERROR $code:<br/>$errormessage<br/>";
257 print_error("rpcerror", '', '', $message);
261 if (empty($remoteuser) or empty($remoteuser->username
)) {
262 print_error('unknownerror', 'mnet');
266 if (user_not_fully_set_up($remoteuser, false)) {
267 print_error('notenoughidpinfo', 'mnet');
271 $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
273 $remoteuser->auth
= 'mnet';
274 $remoteuser->wwwroot
= $remotepeer->wwwroot
;
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
);
281 if (empty($remoteuser->lang
)) {
282 if (!empty($CFG->lang
)) {
283 $remoteuser->lang
= $CFG->lang
;
285 $remoteuser->lang
= 'en';
290 // get the local record for the remote user
291 $localuser = $DB->get_record('user', array('username'=>$remoteuser->username
, 'mnethostid'=>$remotehost->id
));
293 // add the remote user to the database if necessary, and if allowed
294 // TODO: refactor into a separate function
295 if (empty($localuser) ||
! $localuser->id
) {
297 if (empty($this->config->auto_add_remote_users)) {
298 print_error('nolocaluser', 'mnet');
299 } See MDL-21327 for why this is commented out
301 $remoteuser->mnethostid
= $remotehost->id
;
302 $remoteuser->firstaccess
= 0;
303 $remoteuser->confirmed
= 1;
305 $remoteuser->id
= user_create_user($remoteuser, false);
307 $localuser = $remoteuser;
310 // check sso access control list for permission first
311 if (!$this->can_login_remotely($localuser->username
, $remotehost->id
)) {
312 print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username
, 'host'=>$remotehost->name
));
315 $fs = get_file_storage();
317 // update the local user record with remote user data
318 foreach ((array) $remoteuser as $key => $val) {
320 if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages
) and isset($remoteuser->picture
)) {
321 // update the user picture if there is a newer verion at the identity provider
322 $usercontext = context_user
::instance($localuser->id
, MUST_EXIST
);
323 if ($usericonfile = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.png')) {
324 $localtimemodified = $usericonfile->get_timemodified();
325 } else if ($usericonfile = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.jpg')) {
326 $localtimemodified = $usericonfile->get_timemodified();
328 $localtimemodified = 0;
331 if (!empty($val) and $localtimemodified < $val) {
332 mnet_debug('refetching the user picture from the identity provider host');
333 $fetchrequest = new mnet_xmlrpc_client();
334 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
335 $fetchrequest->add_param($localuser->username
);
336 if ($fetchrequest->send($remotepeer) === true) {
337 if (strlen($fetchrequest->response
['f1']) > 0) {
338 $imagefilename = $CFG->tempdir
. '/mnet-usericon-' . $localuser->id
;
339 $imagecontents = base64_decode($fetchrequest->response
['f1']);
340 file_put_contents($imagefilename, $imagecontents);
341 if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
342 $localuser->picture
= $newrev;
344 unlink($imagefilename);
346 // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
347 // the mimetype information provided is ignored and the type of the file is detected
348 // by process_new_icon()
353 if($key == 'myhosts') {
354 $localuser->mnet_foreign_host_array
= array();
355 foreach($val as $rhost) {
356 $name = clean_param($rhost['name'], PARAM_ALPHANUM
);
357 $url = clean_param($rhost['url'], PARAM_URL
);
358 $count = clean_param($rhost['count'], PARAM_INT
);
359 $url_is_local = stristr($url , $CFG->wwwroot
);
360 if (!empty($name) && !empty($count) && empty($url_is_local)) {
361 $localuser->mnet_foreign_host_array
[] = array('name' => $name,
368 $localuser->{$key} = $val;
371 $localuser->mnethostid
= $remotepeer->id
;
372 user_update_user($localuser, false);
375 // repeat customer! let the IDP know about enrolments
376 // we have for this user.
377 // set up the RPC request
378 $mnetrequest = new mnet_xmlrpc_client();
379 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
381 // pass username and an assoc array of "my courses"
382 // with info so that the IDP can maintain mnetservice_enrol_enrolments
383 $mnetrequest->add_param($remoteuser->username
);
384 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
385 $courses = enrol_get_users_courses($localuser->id
, false, $fields, 'visible DESC,sortorder ASC');
386 if (is_array($courses) && !empty($courses)) {
387 // Second request to do the JOINs that we'd have done
388 // inside enrol_get_users_courses() if we had been allowed
390 cc.name AS cat_name, cc.description AS cat_description
392 JOIN {course_categories} cc ON c.category = cc.id
393 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
394 $extra = $DB->get_records_sql($sql);
396 $keys = array_keys($courses);
397 $studentroles = get_archetype_roles('student');
398 if (!empty($studentroles)) {
399 $defaultrole = reset($studentroles);
400 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
401 foreach ($keys AS $id) {
402 if ($courses[$id]->visible
== 0) {
403 unset($courses[$id]);
406 $courses[$id]->cat_id
= $courses[$id]->category
;
407 $courses[$id]->defaultroleid
= $defaultrole->id
;
408 unset($courses[$id]->category
);
409 unset($courses[$id]->visible
);
411 $courses[$id]->cat_name
= $extra[$id]->cat_name
;
412 $courses[$id]->cat_description
= $extra[$id]->cat_description
;
413 $courses[$id]->defaultrolename
= $defaultrole->name
;
415 $courses[$id] = (array)$courses[$id];
418 throw new moodle_exception('unknownrole', 'error', '', 'student');
421 // if the array is empty, send it anyway
422 // we may be clearing out stale entries
425 $mnetrequest->add_param($courses);
427 // Call 0800-RPC Now! -- we don't care too much if it fails
428 // as it's just informational.
429 if ($mnetrequest->send($remotepeer) === false) {
430 // error_log(print_r($mnetrequest->error,1));
439 * creates (or updates) the mnet session once
440 * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
442 * @param stdclass $user the local user (must exist already
443 * @param string $token the jump/land token
444 * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
446 public function update_mnet_session($user, $token, $remotepeer) {
448 $session_gc_maxlifetime = 1440;
449 if (isset($user->session_gc_maxlifetime
)) {
450 $session_gc_maxlifetime = $user->session_gc_maxlifetime
;
452 if (!$mnet_session = $DB->get_record('mnet_session',
453 array('userid'=>$user->id
, 'mnethostid'=>$remotepeer->id
,
454 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
455 $mnet_session = new stdClass();
456 $mnet_session->mnethostid
= $remotepeer->id
;
457 $mnet_session->userid
= $user->id
;
458 $mnet_session->username
= $user->username
;
459 $mnet_session->useragent
= sha1($_SERVER['HTTP_USER_AGENT']);
460 $mnet_session->token
= $token; // Needed to support simultaneous sessions
461 // and preserving DB rec uniqueness
462 $mnet_session->confirm_timeout
= time();
463 $mnet_session->expires
= time() +
(integer)$session_gc_maxlifetime;
464 $mnet_session->session_id
= session_id();
465 $mnet_session->id
= $DB->insert_record('mnet_session', $mnet_session);
467 $mnet_session->expires
= time() +
(integer)$session_gc_maxlifetime;
468 $DB->update_record('mnet_session', $mnet_session);
475 * Invoke this function _on_ the IDP to update it with enrolment info local to
476 * the SP right after calling user_authorise()
478 * Normally called by the SP after calling user_authorise()
480 * @param string $username The username
481 * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses
484 function update_enrolments($username, $courses) {
486 $remoteclient = get_mnet_remote_client();
488 if (empty($username) ||
!is_array($courses)) {
491 // make sure it is a user we have an in active session
493 $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id
), '', 'id, userid');
495 foreach ($mnetsessions as $mnetsession) {
496 if (is_null($userid)) {
497 $userid = $mnetsession->userid
;
500 if ($userid != $mnetsession->userid
) {
501 throw new mnet_server_exception(3, 'authfail_usermismatch');
505 if (empty($courses)) { // no courses? clear out quickly
506 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id
, 'userid'=>$userid));
510 // IMPORTANT: Ask for remoteid as the first element in the query, so
511 // that the array that comes back is indexed on the same field as the
512 // array that we have received from the remote client
513 $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
514 c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
516 FROM {mnetservice_enrol_courses} c
517 LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
518 WHERE e.userid = ? AND c.hostid = ?";
520 $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id
));
522 $local_courseid_array = array();
523 foreach($courses as $ix => $course) {
525 $course['remoteid'] = $course['id'];
526 $course['hostid'] = (int)$remoteclient->id
;
529 // if we do not have the the information about the remote course, it is not available
530 // to us for remote enrolment - skip
531 if (array_key_exists($course['remoteid'], $currentcourses)) {
532 // Pointer to current course:
533 $currentcourse =& $currentcourses[$course['remoteid']];
534 // We have a record - is it up-to-date?
535 $course['id'] = $currentcourse->id
;
539 foreach($course as $key => $value) {
540 if ($currentcourse->$key != $value) {
542 $currentcourse->$key = $value;
547 $DB->update_record('mnetervice_enrol_courses', $currentcourse);
550 if (isset($currentcourse->enrolmentid
) && is_numeric($currentcourse->enrolmentid
)) {
554 unset ($courses[$ix]);
558 // By this point, we should always have a $dataObj->id
559 $local_courseid_array[] = $course['id'];
561 // Do we have a record for this assignment?
563 // Yes - we know about this one already
564 // We don't want to do updates because the new data is probably
565 // 'less complete' than the data we have.
567 // No - create a record
568 $assignObj = new stdClass();
569 $assignObj->userid
= $userid;
570 $assignObj->hostid
= (int)$remoteclient->id
;
571 $assignObj->remotecourseid
= $course['remoteid'];
572 $assignObj->rolename
= $course['defaultrolename'];
573 $assignObj->id
= $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
577 // Clean up courses that the user is no longer enrolled in.
578 if (!empty($local_courseid_array)) {
579 $local_courseid_string = implode(', ', $local_courseid_array);
580 $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
581 $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id
));
585 function prevent_local_passwords() {
590 * Returns true if this authentication plugin is 'internal'.
594 function is_internal() {
599 * Returns true if this authentication plugin can change the user's
604 function can_change_password() {
605 //TODO: it should be able to redirect, right?
610 * Returns the URL for changing the user's pw, or false if the default can
615 function change_password_url() {
620 * Prints a form for configuring this authentication plugin.
622 * This function is called from admin/auth.php, and outputs a full page with
623 * a form for configuring this plugin.
625 * @param object $config
627 * @param array $user_fields
629 function config_form($config, $err, $user_fields) {
637 h2idp.publish as idppublish,
638 h2idp.subscribe as idpsubscribe,
640 h2sp.publish as sppublish,
641 h2sp.subscribe as spsubscribe,
646 {mnet_host2service} h2idp
648 (h.id = h2idp.hostid AND
649 (h2idp.publish = 1 OR
650 h2idp.subscribe = 1))
654 (h2idp.serviceid = idp.id AND
655 idp.name = 'sso_idp')
657 {mnet_host2service} h2sp
659 (h.id = h2sp.hostid AND
665 (h2sp.serviceid = sp.id AND
668 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
669 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
674 $id_providers = array();
675 $service_providers = array();
676 if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id
))) {
677 foreach($resultset as $hostservice) {
678 if(!empty($hostservice->idppublish
) && !empty($hostservice->spsubscribe
)) {
679 $service_providers[]= array('id' => $hostservice->id
, 'name' => $hostservice->hostname
, 'wwwroot' => $hostservice->wwwroot
);
681 if(!empty($hostservice->idpsubscribe
) && !empty($hostservice->sppublish
)) {
682 $id_providers[]= array('id' => $hostservice->id
, 'name' => $hostservice->hostname
, 'wwwroot' => $hostservice->wwwroot
);
687 include "config.html";
691 * Processes and stores configuration data for this authentication plugin.
693 function process_config($config) {
694 // set to defaults if undefined
695 if (!isset ($config->rpc_negotiation_timeout
)) {
696 $config->rpc_negotiation_timeout
= '30';
699 if (!isset ($config->auto_add_remote_users)) {
700 $config->auto_add_remote_users = '0';
701 } See MDL-21327 for why this is commented out
702 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth_mnet');
706 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout
, 'auth_mnet');
712 * Poll the IdP server to let it know that a user it has authenticated is still
717 function keepalive_client() {
719 $cutoff = time() - 300; // TODO - find out what the remote server's session
720 // cutoff is, and preempt that
735 $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id
));
737 if ($immigrants == false) {
741 $usersArray = array();
742 foreach($immigrants as $immigrant) {
743 $usersArray[$immigrant->mnethostid
][] = $immigrant->username
;
746 require_once $CFG->dirroot
. '/mnet/xmlrpc/client.php';
747 foreach($usersArray as $mnethostid => $users) {
748 $mnet_peer = new mnet_peer();
749 $mnet_peer->set_id($mnethostid);
751 $mnet_request = new mnet_xmlrpc_client();
752 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
754 // set $token and $useragent parameters
755 $mnet_request->add_param($users);
757 if ($mnet_request->send($mnet_peer) === true) {
758 if (!isset($mnet_request->response
['code'])) {
759 debugging("Server side error has occured on host $mnethostid");
761 } elseif ($mnet_request->response
['code'] > 0) {
762 debugging($mnet_request->response
['message']);
765 if (!isset($mnet_request->response
['last log id'])) {
766 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
770 debugging("Server side error has occured on host $mnethostid: " .
771 join("\n", $mnet_request->error
));
778 * Receives an array of log entries from an SP and adds them to the mnet_log
781 * @deprecated since Moodle 2.8 Please don't use this function for recording mnet logs.
782 * @param array $array An array of usernames
783 * @return string "All ok" or an error message
785 function refresh_log($array) {
786 debugging('refresh_log() is deprecated, The transfer of logs through mnet are no longer recorded.', DEBUG_DEVELOPER
);
787 return array('code' => 0, 'message' => 'All ok');
791 * Receives an array of usernames from a remote machine and prods their
792 * sessions to keep them alive
794 * @param array $array An array of usernames
795 * @return string "All ok" or an error message
797 function keepalive_server($array) {
799 $remoteclient = get_mnet_remote_client();
801 // We don't want to output anything to the client machine
804 // We'll get session records in batches of 30
805 $superArray = array_chunk($array, 30);
809 foreach($superArray as $subArray) {
810 $subArray = array_values($subArray);
811 $instring = "('".implode("', '",$subArray)."')";
812 $query = "select id, session_id, username from {mnet_session} where username in $instring";
813 $results = $DB->get_records_sql($query);
815 if ($results == false) {
816 // We seem to have a username that breaks our query:
817 // TODO: Handle this error appropriately
818 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
820 foreach($results as $emigrant) {
821 \core\session\manager
::touch_session($emigrant->session_id
);
826 $end = ob_end_clean();
828 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id
);
829 return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id
);
833 * Cron function will be called automatically by cron.php every 5 minutes
840 // run the keepalive client
841 $this->keepalive_client();
843 $random100 = rand(0,100);
844 if ($random100 < 10) { // Approximately 10% of the time.
845 // nuke olden sessions
846 $longtime = time() - (1 * 3600 * 24);
847 $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
852 * Cleanup any remote mnet_sessions, kill the local mnet_session data
854 * This is called by require_logout in moodlelib
858 function prelogout_hook() {
861 if (!is_enabled_auth('mnet')) {
865 // If the user is local to this Moodle:
866 if ($USER->mnethostid
== $this->mnet
->id
) {
867 $this->kill_children($USER->username
, sha1($_SERVER['HTTP_USER_AGENT']));
869 // Else the user has hit 'logout' at a Service Provider Moodle:
871 $this->kill_parent($USER->username
, sha1($_SERVER['HTTP_USER_AGENT']));
877 * The SP uses this function to kill the session on the parent IdP
879 * @param string $username Username for session to kill
880 * @param string $useragent SHA1 hash of user agent to look for
881 * @return string A plaintext report of what has happened
883 function kill_parent($username, $useragent) {
884 global $CFG, $USER, $DB;
886 require_once $CFG->dirroot
.'/mnet/xmlrpc/client.php';
897 $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid
));
899 $ignore = $DB->delete_records('mnet_session',
900 array('username'=>$username,
901 'useragent'=>$useragent,
902 'mnethostid'=>$USER->mnethostid
));
904 if (false != $mnetsessions) {
905 $mnet_peer = new mnet_peer();
906 $mnet_peer->set_id($USER->mnethostid
);
908 $mnet_request = new mnet_xmlrpc_client();
909 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
911 // set $token and $useragent parameters
912 $mnet_request->add_param($username);
913 $mnet_request->add_param($useragent);
914 if ($mnet_request->send($mnet_peer) === false) {
915 debugging(join("\n", $mnet_request->error
));
924 * The IdP uses this function to kill child sessions on other hosts
926 * @param string $username Username for session to kill
927 * @param string $useragent SHA1 hash of user agent to look for
928 * @return string A plaintext report of what has happened
930 function kill_children($username, $useragent) {
931 global $CFG, $USER, $DB;
932 $remoteclient = null;
933 if (defined('MNET_SERVER')) {
934 $remoteclient = get_mnet_remote_client();
936 require_once $CFG->dirroot
.'/mnet/xmlrpc/client.php';
938 $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id
, 'username'=>$username));
942 $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
944 if (false == $mnetsessions) {
945 $returnstring .= "Could find no remote sessions\n";
946 $mnetsessions = array();
949 foreach($mnetsessions as $mnetsession) {
950 // If this script is being executed by a remote peer, that means the user has clicked
951 // logout on that peer, and the session on that peer can be deleted natively.
953 if (isset($remoteclient->id
) && ($mnetsession->mnethostid
== $remoteclient->id
)) {
956 $returnstring .= "Deleting session\n";
958 $mnet_peer = new mnet_peer();
959 $mnet_peer->set_id($mnetsession->mnethostid
);
961 $mnet_request = new mnet_xmlrpc_client();
962 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
964 // set $token and $useragent parameters
965 $mnet_request->add_param($username);
966 $mnet_request->add_param($useragent);
967 if ($mnet_request->send($mnet_peer) === false) {
968 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
969 join("\n", $mnet_request->error
));
973 $ignore = $DB->delete_records('mnet_session',
974 array('useragent'=>$useragent, 'userid'=>$userid));
976 if (isset($remoteclient) && isset($remoteclient->id
)) {
977 \core\session\manager
::kill_user_sessions($userid);
979 return $returnstring;
983 * When the IdP requests that child sessions are terminated,
984 * this function will be called on each of the child hosts. The machine that
985 * calls the function (over xmlrpc) provides us with the mnethostid we need.
987 * @param string $username Username for session to kill
988 * @param string $useragent SHA1 hash of user agent to look for
989 * @return bool True on success
991 function kill_child($username, $useragent) {
993 $remoteclient = get_mnet_remote_client();
994 $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id
, 'useragent'=>$useragent));
995 $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id
, 'useragent'=>$useragent));
996 if (false != $session) {
997 \core\session\manager
::kill_session($session->session_id
);
1004 * To delete a host, we must delete all current sessions that users from
1005 * that host are currently engaged in.
1007 * @param string $sessionidarray An array of session hashes
1008 * @return bool True on success
1010 function end_local_sessions(&$sessionArray) {
1012 if (is_array($sessionArray)) {
1013 while($session = array_pop($sessionArray)) {
1014 \core\session\manager
::kill_session($session->session_id
);
1022 * Returns the user's profile image info
1024 * If the user exists and has a profile picture, the returned array will contain keys:
1025 * f1 - the content of the default 100x100px image
1026 * f1_mimetype - the mimetype of the f1 file
1027 * f2 - the content of the 35x35px variant of the image
1028 * f2_mimetype - the mimetype of the f2 file
1030 * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1032 * @see process_new_icon()
1033 * @uses mnet_remote_client callable via MNet XML-RPC
1034 * @param int $username The id of the user
1035 * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1037 function fetch_user_image($username) {
1040 if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id
))) {
1041 $fs = get_file_storage();
1042 $usercontext = context_user
::instance($user->id
, MUST_EXIST
);
1044 if ($f1 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.png')) {
1045 $return['f1'] = base64_encode($f1->get_content());
1046 $return['f1_mimetype'] = $f1->get_mimetype();
1047 } else if ($f1 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.jpg')) {
1048 $return['f1'] = base64_encode($f1->get_content());
1049 $return['f1_mimetype'] = $f1->get_mimetype();
1051 if ($f2 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f2.png')) {
1052 $return['f2'] = base64_encode($f2->get_content());
1053 $return['f2_mimetype'] = $f2->get_mimetype();
1054 } else if ($f2 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f2.jpg')) {
1055 $return['f2'] = base64_encode($f2->get_content());
1056 $return['f2_mimetype'] = $f2->get_mimetype();
1064 * Returns the theme information and logo url as strings.
1066 * @return string The theme info
1068 function fetch_theme_info() {
1071 $themename = "$CFG->theme";
1072 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1074 $return['themename'] = $themename;
1075 $return['logourl'] = $logourl;
1080 * Determines if an MNET host is providing the nominated service.
1082 * @param int $mnethostid The id of the remote host
1083 * @param string $servicename The name of the service
1084 * @return bool Whether the service is available on the remote host
1086 function has_service($mnethostid, $servicename) {
1091 svc.id as serviceid,
1100 {mnet_host2service} h2s
1103 h.id = h2s.hostid AND
1105 h2s.serviceid = svc.id AND
1107 h2s.subscribe = '1'";
1109 return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1113 * Checks the MNET access control table to see if the username/mnethost
1114 * is permitted to login to this moodle.
1116 * @param string $username The username
1117 * @param int $mnethostid The id of the remote mnethost
1118 * @return bool Whether the user can login from the remote host
1120 function can_login_remotely($username, $mnethostid) {
1123 $accessctrl = 'allow';
1124 $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1125 if (!empty($aclrecord)) {
1126 $accessctrl = $aclrecord->accessctrl
;
1128 return $accessctrl == 'allow';
1131 function logoutpage_hook() {
1132 global $USER, $CFG, $redirect, $DB;
1134 if (!empty($USER->mnethostid
) and $USER->mnethostid
!= $CFG->mnet_localhost_id
) {
1135 $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid
));
1136 $redirect = $host->wwwroot
.'/';
1141 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1143 * @param object $logline The log information to be trimmed
1144 * @return object The passed logline object trimmed to not exceed storable limits
1146 function trim_logline ($logline) {
1147 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1149 foreach ($limits as $property => $limit) {
1150 if (isset($logline->$property)) {
1151 $logline->$property = substr($logline->$property, 0, $limit);
1159 * Returns a list of potential IdPs that this authentication plugin supports.
1160 * This is used to provide links on the login page.
1162 * @param string $wantsurl the relative url fragment the user wants to get to. You can use this to compose a returnurl, for example
1164 * @return array like:
1167 * 'url' => 'http://someurl',
1168 * 'icon' => new pix_icon(...),
1169 * 'name' => get_string('somename', 'auth_yourplugin'),
1173 function loginpage_idp_list($wantsurl) {
1176 // strip off wwwroot, since the remote site will prefix it's return url with this
1177 $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot
, '/') . '|' . preg_quote($CFG->httpswwwroot
, '/') . ')/', '', $wantsurl);
1179 $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1181 JOIN {mnet_host2service} m ON h.id = m.hostid
1182 JOIN {mnet_service} s ON s.id = m.serviceid
1183 JOIN {mnet_application} a ON h.applicationid = a.id
1184 WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1185 $params = array('sso_sp', 0, 1);
1187 if (!empty($CFG->mnet_all_hosts_id
)) {
1188 $sql .= " AND h.id <> ?";
1189 $params[] = $CFG->mnet_all_hosts_id
;
1192 if (!$hosts = $DB->get_records_sql($sql, $params)) {
1197 foreach ($hosts as $host) {
1199 'url' => new moodle_url($host->wwwroot
. $host->sso_jump_url
, array('hostwwwroot' => $CFG->wwwroot
, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1200 'icon' => new pix_icon('i/' . $host->application
. '_host', $host->name
),
1201 'name' => $host->name
,