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 function auth_plugin_mnet() {
39 $this->authtype
= 'mnet';
40 $this->config
= get_config('auth_mnet');
41 $this->mnet
= get_mnet_environment();
45 * This function is normally used to determine if the username and password
46 * are correct for local logins. Always returns false, as local users do not
47 * need to login over mnet xmlrpc.
49 * @param string $username The username
50 * @param string $password The password
51 * @return bool Authentication success or failure.
53 function user_login($username, $password) {
54 return false; // print_error("mnetlocal");
58 * Return user data for the provided token, compare with user_agent string.
60 * @param string $token The unique ID provided by remotehost.
61 * @param string $UA User Agent string.
62 * @return array $userdata Array of user info for remote host
64 function user_authorise($token, $useragent) {
65 global $CFG, $SITE, $DB;
66 $remoteclient = get_mnet_remote_client();
67 require_once $CFG->dirroot
. '/mnet/xmlrpc/serverlib.php';
69 $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent));
70 if (empty($mnet_session)) {
71 throw new mnet_server_exception(1, 'authfail_nosessionexists');
74 // check session confirm timeout
75 if ($mnet_session->confirm_timeout
< time()) {
76 throw new mnet_server_exception(2, 'authfail_sessiontimedout');
79 // session okay, try getting the user
80 if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid
))) {
81 throw new mnet_server_exception(3, 'authfail_usermismatch');
84 $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient));
87 $userdata['auth'] = 'mnet';
88 $userdata['wwwroot'] = $this->mnet
->wwwroot
;
89 $userdata['session.gc_maxlifetime'] = ini_get('session.gc_maxlifetime');
91 if (array_key_exists('picture', $userdata) && !empty($user->picture
)) {
92 $fs = get_file_storage();
93 $usercontext = context_user
::instance($user->id
, MUST_EXIST
);
94 if ($usericonfile = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.png')) {
95 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
96 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
97 } else if ($usericonfile = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.jpg')) {
98 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
99 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
103 $userdata['myhosts'] = array();
104 if ($courses = enrol_get_users_courses($user->id
, false)) {
105 $userdata['myhosts'][] = array('name'=> $SITE->shortname
, 'url' => $CFG->wwwroot
, 'count' => count($courses));
108 $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
110 FROM {mnetservice_enrol_courses} c
111 JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
112 JOIN {mnet_host} h ON h.id = c.hostid
113 WHERE e.userid = ? AND c.hostid = ?
114 GROUP BY h.name, h.wwwroot, h.id";
116 if ($courses = $DB->get_records_sql($sql, array($user->id
, $remoteclient->id
))) {
117 foreach($courses as $course) {
118 $userdata['myhosts'][] = array('name'=> $course->hostname
, 'url' => $CFG->wwwroot
.'/auth/mnet/jump.php?hostid='.$course->hostid
, 'count' => $course->count
);
126 * Generate a random string for use as an RPC session token.
128 function generate_token() {
129 return sha1(str_shuffle('' . mt_rand() . time()));
133 * Starts an RPC jump session and returns the jump redirect URL.
135 * @param int $mnethostid id of the mnet host to jump to
136 * @param string $wantsurl url to redirect to after the jump (usually on remote system)
137 * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
138 * rather than somewhere inside *its* wwwroot
140 function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
141 global $CFG, $USER, $DB;
142 require_once $CFG->dirroot
. '/mnet/xmlrpc/client.php';
144 if (\core\session\manager
::is_loggedinas()) {
145 print_error('notpermittedtojumpas', 'mnet');
148 // check remote login permissions
149 if (! has_capability('moodle/site:mnetlogintoremote', context_system
::instance())
150 or is_mnet_remote_user($USER)
153 print_error('notpermittedtojump', 'mnet');
156 // check for SSO publish permission first
157 if ($this->has_service($mnethostid, 'sso_sp') == false) {
158 print_error('hostnotconfiguredforsso', 'mnet');
161 // set RPC timeout to 30 seconds if not configured
162 if (empty($this->config
->rpc_negotiation_timeout
)) {
163 $this->config
->rpc_negotiation_timeout
= 30;
164 set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
168 $mnet_peer = new mnet_peer();
169 $mnet_peer->set_id($mnethostid);
171 // set up the session
172 $mnet_session = $DB->get_record('mnet_session',
173 array('userid'=>$USER->id
, 'mnethostid'=>$mnethostid,
174 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
175 if ($mnet_session == false) {
176 $mnet_session = new stdClass();
177 $mnet_session->mnethostid
= $mnethostid;
178 $mnet_session->userid
= $USER->id
;
179 $mnet_session->username
= $USER->username
;
180 $mnet_session->useragent
= sha1($_SERVER['HTTP_USER_AGENT']);
181 $mnet_session->token
= $this->generate_token();
182 $mnet_session->confirm_timeout
= time() +
$this->config
->rpc_negotiation_timeout
;
183 $mnet_session->expires
= time() +
(integer)ini_get('session.gc_maxlifetime');
184 $mnet_session->session_id
= session_id();
185 $mnet_session->id
= $DB->insert_record('mnet_session', $mnet_session);
187 $mnet_session->useragent
= sha1($_SERVER['HTTP_USER_AGENT']);
188 $mnet_session->token
= $this->generate_token();
189 $mnet_session->confirm_timeout
= time() +
$this->config
->rpc_negotiation_timeout
;
190 $mnet_session->expires
= time() +
(integer)ini_get('session.gc_maxlifetime');
191 $mnet_session->session_id
= session_id();
192 $DB->update_record('mnet_session', $mnet_session);
195 // construct the redirection URL
196 //$transport = mnet_get_protocol($mnet_peer->transport);
197 $wantsurl = urlencode($wantsurl);
198 $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
199 if ($wantsurlbackhere) {
200 $url .= '&remoteurl=1';
207 * This function confirms the remote (ID provider) host's mnet session
208 * by communicating the token and UA over the XMLRPC transport layer, and
209 * returns the local user record on success.
211 * @param string $token The random session token.
212 * @param mnet_peer $remotepeer The ID provider mnet_peer object.
213 * @return array The local user record.
215 function confirm_mnet_session($token, $remotepeer) {
217 require_once $CFG->dirroot
. '/mnet/xmlrpc/client.php';
218 require_once $CFG->libdir
. '/gdlib.php';
219 require_once($CFG->dirroot
.'/user/lib.php');
221 // verify the remote host is configured locally before attempting RPC call
222 if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot
, 'deleted' => 0))) {
223 print_error('notpermittedtoland', 'mnet');
226 // set up the RPC request
227 $mnetrequest = new mnet_xmlrpc_client();
228 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
230 // set $token and $useragent parameters
231 $mnetrequest->add_param($token);
232 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
234 // Thunderbirds are go! Do RPC call and store response
235 if ($mnetrequest->send($remotepeer) === true) {
236 $remoteuser = (object) $mnetrequest->response
;
238 foreach ($mnetrequest->error
as $errormessage) {
239 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
242 print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot
, format_string($site->fullname
));
245 $message .= "ERROR $code:<br/>$errormessage<br/>";
247 print_error("rpcerror", '', '', $message);
251 if (empty($remoteuser) or empty($remoteuser->username
)) {
252 print_error('unknownerror', 'mnet');
256 if (user_not_fully_set_up($remoteuser)) {
257 print_error('notenoughidpinfo', 'mnet');
261 $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
263 $remoteuser->auth
= 'mnet';
264 $remoteuser->wwwroot
= $remotepeer->wwwroot
;
266 // the user may roam from Moodle 1.x where lang has _utf8 suffix
267 // also, make sure that the lang is actually installed, otherwise set site default
268 if (isset($remoteuser->lang
)) {
269 $remoteuser->lang
= clean_param(str_replace('_utf8', '', $remoteuser->lang
), PARAM_LANG
);
271 if (empty($remoteuser->lang
)) {
272 if (!empty($CFG->lang
)) {
273 $remoteuser->lang
= $CFG->lang
;
275 $remoteuser->lang
= 'en';
280 // get the local record for the remote user
281 $localuser = $DB->get_record('user', array('username'=>$remoteuser->username
, 'mnethostid'=>$remotehost->id
));
283 // add the remote user to the database if necessary, and if allowed
284 // TODO: refactor into a separate function
285 if (empty($localuser) ||
! $localuser->id
) {
287 if (empty($this->config->auto_add_remote_users)) {
288 print_error('nolocaluser', 'mnet');
289 } See MDL-21327 for why this is commented out
291 $remoteuser->mnethostid
= $remotehost->id
;
292 $remoteuser->firstaccess
= time(); // First time user in this server, grab it here
293 $remoteuser->confirmed
= 1;
295 $remoteuser->id
= $DB->insert_record('user', $remoteuser);
297 $localuser = $remoteuser;
300 // check sso access control list for permission first
301 if (!$this->can_login_remotely($localuser->username
, $remotehost->id
)) {
302 print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username
, 'host'=>$remotehost->name
));
305 $fs = get_file_storage();
307 // update the local user record with remote user data
308 foreach ((array) $remoteuser as $key => $val) {
310 if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages
) and isset($remoteuser->picture
)) {
311 // update the user picture if there is a newer verion at the identity provider
312 $usercontext = context_user
::instance($localuser->id
, MUST_EXIST
);
313 if ($usericonfile = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.png')) {
314 $localtimemodified = $usericonfile->get_timemodified();
315 } else if ($usericonfile = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.jpg')) {
316 $localtimemodified = $usericonfile->get_timemodified();
318 $localtimemodified = 0;
321 if (!empty($val) and $localtimemodified < $val) {
322 mnet_debug('refetching the user picture from the identity provider host');
323 $fetchrequest = new mnet_xmlrpc_client();
324 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
325 $fetchrequest->add_param($localuser->username
);
326 if ($fetchrequest->send($remotepeer) === true) {
327 if (strlen($fetchrequest->response
['f1']) > 0) {
328 $imagefilename = $CFG->tempdir
. '/mnet-usericon-' . $localuser->id
;
329 $imagecontents = base64_decode($fetchrequest->response
['f1']);
330 file_put_contents($imagefilename, $imagecontents);
331 if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
332 $localuser->picture
= $newrev;
334 unlink($imagefilename);
336 // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
337 // the mimetype information provided is ignored and the type of the file is detected
338 // by process_new_icon()
343 if($key == 'myhosts') {
344 $localuser->mnet_foreign_host_array
= array();
345 foreach($val as $rhost) {
346 $name = clean_param($rhost['name'], PARAM_ALPHANUM
);
347 $url = clean_param($rhost['url'], PARAM_URL
);
348 $count = clean_param($rhost['count'], PARAM_INT
);
349 $url_is_local = stristr($url , $CFG->wwwroot
);
350 if (!empty($name) && !empty($count) && empty($url_is_local)) {
351 $localuser->mnet_foreign_host_array
[] = array('name' => $name,
358 $localuser->{$key} = $val;
361 $localuser->mnethostid
= $remotepeer->id
;
362 if (empty($localuser->firstaccess
)) { // Now firstaccess, grab it here
363 $localuser->firstaccess
= time();
365 user_update_user($localuser, false);
368 // repeat customer! let the IDP know about enrolments
369 // we have for this user.
370 // set up the RPC request
371 $mnetrequest = new mnet_xmlrpc_client();
372 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
374 // pass username and an assoc array of "my courses"
375 // with info so that the IDP can maintain mnetservice_enrol_enrolments
376 $mnetrequest->add_param($remoteuser->username
);
377 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
378 $courses = enrol_get_users_courses($localuser->id
, false, $fields, 'visible DESC,sortorder ASC');
379 if (is_array($courses) && !empty($courses)) {
380 // Second request to do the JOINs that we'd have done
381 // inside enrol_get_users_courses() if we had been allowed
383 cc.name AS cat_name, cc.description AS cat_description
385 JOIN {course_categories} cc ON c.category = cc.id
386 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
387 $extra = $DB->get_records_sql($sql);
389 $keys = array_keys($courses);
390 $studentroles = get_archetype_roles('student');
391 if (!empty($studentroles)) {
392 $defaultrole = reset($studentroles);
393 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
394 foreach ($keys AS $id) {
395 if ($courses[$id]->visible
== 0) {
396 unset($courses[$id]);
399 $courses[$id]->cat_id
= $courses[$id]->category
;
400 $courses[$id]->defaultroleid
= $defaultrole->id
;
401 unset($courses[$id]->category
);
402 unset($courses[$id]->visible
);
404 $courses[$id]->cat_name
= $extra[$id]->cat_name
;
405 $courses[$id]->cat_description
= $extra[$id]->cat_description
;
406 $courses[$id]->defaultrolename
= $defaultrole->name
;
408 $courses[$id] = (array)$courses[$id];
411 throw new moodle_exception('unknownrole', 'error', '', 'student');
414 // if the array is empty, send it anyway
415 // we may be clearing out stale entries
418 $mnetrequest->add_param($courses);
420 // Call 0800-RPC Now! -- we don't care too much if it fails
421 // as it's just informational.
422 if ($mnetrequest->send($remotepeer) === false) {
423 // error_log(print_r($mnetrequest->error,1));
432 * creates (or updates) the mnet session once
433 * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
435 * @param stdclass $user the local user (must exist already
436 * @param string $token the jump/land token
437 * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
439 public function update_mnet_session($user, $token, $remotepeer) {
441 $session_gc_maxlifetime = 1440;
442 if (isset($user->session_gc_maxlifetime
)) {
443 $session_gc_maxlifetime = $user->session_gc_maxlifetime
;
445 if (!$mnet_session = $DB->get_record('mnet_session',
446 array('userid'=>$user->id
, 'mnethostid'=>$remotepeer->id
,
447 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
448 $mnet_session = new stdClass();
449 $mnet_session->mnethostid
= $remotepeer->id
;
450 $mnet_session->userid
= $user->id
;
451 $mnet_session->username
= $user->username
;
452 $mnet_session->useragent
= sha1($_SERVER['HTTP_USER_AGENT']);
453 $mnet_session->token
= $token; // Needed to support simultaneous sessions
454 // and preserving DB rec uniqueness
455 $mnet_session->confirm_timeout
= time();
456 $mnet_session->expires
= time() +
(integer)$session_gc_maxlifetime;
457 $mnet_session->session_id
= session_id();
458 $mnet_session->id
= $DB->insert_record('mnet_session', $mnet_session);
460 $mnet_session->expires
= time() +
(integer)$session_gc_maxlifetime;
461 $DB->update_record('mnet_session', $mnet_session);
468 * Invoke this function _on_ the IDP to update it with enrolment info local to
469 * the SP right after calling user_authorise()
471 * Normally called by the SP after calling user_authorise()
473 * @param string $username The username
474 * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses
477 function update_enrolments($username, $courses) {
479 $remoteclient = get_mnet_remote_client();
481 if (empty($username) ||
!is_array($courses)) {
484 // make sure it is a user we have an in active session
486 $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id
), '', 'id, userid');
488 foreach ($mnetsessions as $mnetsession) {
489 if (is_null($userid)) {
490 $userid = $mnetsession->userid
;
493 if ($userid != $mnetsession->userid
) {
494 throw new mnet_server_exception(3, 'authfail_usermismatch');
498 if (empty($courses)) { // no courses? clear out quickly
499 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id
, 'userid'=>$userid));
503 // IMPORTANT: Ask for remoteid as the first element in the query, so
504 // that the array that comes back is indexed on the same field as the
505 // array that we have received from the remote client
506 $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
507 c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
509 FROM {mnetservice_enrol_courses} c
510 LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
511 WHERE e.userid = ? AND c.hostid = ?";
513 $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id
));
515 $local_courseid_array = array();
516 foreach($courses as $ix => $course) {
518 $course['remoteid'] = $course['id'];
519 $course['hostid'] = (int)$remoteclient->id
;
522 // if we do not have the the information about the remote course, it is not available
523 // to us for remote enrolment - skip
524 if (array_key_exists($course['remoteid'], $currentcourses)) {
525 // Pointer to current course:
526 $currentcourse =& $currentcourses[$course['remoteid']];
527 // We have a record - is it up-to-date?
528 $course['id'] = $currentcourse->id
;
532 foreach($course as $key => $value) {
533 if ($currentcourse->$key != $value) {
535 $currentcourse->$key = $value;
540 $DB->update_record('mnetervice_enrol_courses', $currentcourse);
543 if (isset($currentcourse->enrolmentid
) && is_numeric($currentcourse->enrolmentid
)) {
547 unset ($courses[$ix]);
551 // By this point, we should always have a $dataObj->id
552 $local_courseid_array[] = $course['id'];
554 // Do we have a record for this assignment?
556 // Yes - we know about this one already
557 // We don't want to do updates because the new data is probably
558 // 'less complete' than the data we have.
560 // No - create a record
561 $assignObj = new stdClass();
562 $assignObj->userid
= $userid;
563 $assignObj->hostid
= (int)$remoteclient->id
;
564 $assignObj->remotecourseid
= $course['remoteid'];
565 $assignObj->rolename
= $course['defaultrolename'];
566 $assignObj->id
= $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
570 // Clean up courses that the user is no longer enrolled in.
571 if (!empty($local_courseid_array)) {
572 $local_courseid_string = implode(', ', $local_courseid_array);
573 $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
574 $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id
));
578 function prevent_local_passwords() {
583 * Returns true if this authentication plugin is 'internal'.
587 function is_internal() {
592 * Returns true if this authentication plugin can change the user's
597 function can_change_password() {
598 //TODO: it should be able to redirect, right?
603 * Returns the URL for changing the user's pw, or false if the default can
608 function change_password_url() {
613 * Prints a form for configuring this authentication plugin.
615 * This function is called from admin/auth.php, and outputs a full page with
616 * a form for configuring this plugin.
618 * @param object $config
620 * @param array $user_fields
622 function config_form($config, $err, $user_fields) {
630 h2idp.publish as idppublish,
631 h2idp.subscribe as idpsubscribe,
633 h2sp.publish as sppublish,
634 h2sp.subscribe as spsubscribe,
639 {mnet_host2service} h2idp
641 (h.id = h2idp.hostid AND
642 (h2idp.publish = 1 OR
643 h2idp.subscribe = 1))
647 (h2idp.serviceid = idp.id AND
648 idp.name = 'sso_idp')
650 {mnet_host2service} h2sp
652 (h.id = h2sp.hostid AND
658 (h2sp.serviceid = sp.id AND
661 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
662 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
667 $id_providers = array();
668 $service_providers = array();
669 if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id
))) {
670 foreach($resultset as $hostservice) {
671 if(!empty($hostservice->idppublish
) && !empty($hostservice->spsubscribe
)) {
672 $service_providers[]= array('id' => $hostservice->id
, 'name' => $hostservice->hostname
, 'wwwroot' => $hostservice->wwwroot
);
674 if(!empty($hostservice->idpsubscribe
) && !empty($hostservice->sppublish
)) {
675 $id_providers[]= array('id' => $hostservice->id
, 'name' => $hostservice->hostname
, 'wwwroot' => $hostservice->wwwroot
);
680 include "config.html";
684 * Processes and stores configuration data for this authentication plugin.
686 function process_config($config) {
687 // set to defaults if undefined
688 if (!isset ($config->rpc_negotiation_timeout
)) {
689 $config->rpc_negotiation_timeout
= '30';
692 if (!isset ($config->auto_add_remote_users)) {
693 $config->auto_add_remote_users = '0';
694 } See MDL-21327 for why this is commented out
695 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth_mnet');
699 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout
, 'auth_mnet');
705 * Poll the IdP server to let it know that a user it has authenticated is still
710 function keepalive_client() {
712 $cutoff = time() - 300; // TODO - find out what the remote server's session
713 // cutoff is, and preempt that
728 $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id
));
730 if ($immigrants == false) {
734 $usersArray = array();
735 foreach($immigrants as $immigrant) {
736 $usersArray[$immigrant->mnethostid
][] = $immigrant->username
;
739 require_once $CFG->dirroot
. '/mnet/xmlrpc/client.php';
740 foreach($usersArray as $mnethostid => $users) {
741 $mnet_peer = new mnet_peer();
742 $mnet_peer->set_id($mnethostid);
744 $mnet_request = new mnet_xmlrpc_client();
745 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
747 // set $token and $useragent parameters
748 $mnet_request->add_param($users);
750 if ($mnet_request->send($mnet_peer) === true) {
751 if (!isset($mnet_request->response
['code'])) {
752 debugging("Server side error has occured on host $mnethostid");
754 } elseif ($mnet_request->response
['code'] > 0) {
755 debugging($mnet_request->response
['message']);
758 if (!isset($mnet_request->response
['last log id'])) {
759 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
763 debugging("Server side error has occured on host $mnethostid: " .
764 join("\n", $mnet_request->error
));
769 l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
770 l.action, l.url, l.info, u.username
773 INNER JOIN {log} l on l.userid = u.id
777 AND l.course IS NOT NULL
780 $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response
['last log id']), 0, 500);
782 if ($mnethostlogs == false) {
786 $processedlogs = array();
788 foreach($mnethostlogs as $hostlog) {
790 // Get impersonalised course information. If it is cached there will be no DB queries.
791 $modinfo = get_fast_modinfo($hostlog->course
, -1);
792 $hostlog->coursename
= $modinfo->get_course()->fullname
;
793 if (!empty($hostlog->cmid
) && isset($modinfo->cms
[$hostlog->cmid
])) {
794 $hostlog->resource_name
= $modinfo->cms
[$hostlog->cmid
]->name
;
796 $hostlog->resource_name
= '';
798 } catch (moodle_exception
$e) {
803 $processedlogs[] = array (
804 'remoteid' => $hostlog->remoteid
,
805 'time' => $hostlog->time
,
806 'userid' => $hostlog->userid
,
807 'ip' => $hostlog->ip
,
808 'course' => $hostlog->course
,
809 'coursename' => $hostlog->coursename
,
810 'module' => $hostlog->module
,
811 'cmid' => $hostlog->cmid
,
812 'action' => $hostlog->action
,
813 'url' => $hostlog->url
,
814 'info' => $hostlog->info
,
815 'resource_name' => $hostlog->resource_name
,
816 'username' => $hostlog->username
822 $mnet_request = new mnet_xmlrpc_client();
823 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
825 // set $token and $useragent parameters
826 $mnet_request->add_param($processedlogs);
828 if ($mnet_request->send($mnet_peer) === true) {
829 if ($mnet_request->response
['code'] > 0) {
830 debugging($mnet_request->response
['message']);
833 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error
));
839 * Receives an array of log entries from an SP and adds them to the mnet_log
842 * @param array $array An array of usernames
843 * @return string "All ok" or an error message
845 function refresh_log($array) {
847 $remoteclient = get_mnet_remote_client();
849 // We don't want to output anything to the client machine
853 $transaction = $DB->start_delegated_transaction();
854 $useridarray = array();
856 foreach($array as $logEntry) {
857 $logEntryObj = (object)$logEntry;
858 $logEntryObj->hostid
= $remoteclient->id
;
860 if (isset($useridarray[$logEntryObj->username
])) {
861 $logEntryObj->userid
= $useridarray[$logEntryObj->username
];
863 $logEntryObj->userid
= $DB->get_field('user', 'id', array('username'=>$logEntryObj->username
, 'mnethostid'=>(int)$logEntryObj->hostid
));
864 if ($logEntryObj->userid
== false) {
865 $logEntryObj->userid
= 0;
867 $useridarray[$logEntryObj->username
] = $logEntryObj->userid
;
870 unset($logEntryObj->username
);
872 $logEntryObj = $this->trim_logline($logEntryObj);
873 $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
876 $remoteclient->last_log_id
= $logEntryObj->remoteid
;
878 $returnString .= 'Record with id '.$logEntryObj->remoteid
." failed to insert.\n";
881 $remoteclient->commit();
882 $transaction->allow_commit();
884 $end = ob_end_clean();
886 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
887 return array('code' => 1, 'message' => $returnString);
891 * Receives an array of usernames from a remote machine and prods their
892 * sessions to keep them alive
894 * @param array $array An array of usernames
895 * @return string "All ok" or an error message
897 function keepalive_server($array) {
899 $remoteclient = get_mnet_remote_client();
901 // We don't want to output anything to the client machine
904 // We'll get session records in batches of 30
905 $superArray = array_chunk($array, 30);
909 foreach($superArray as $subArray) {
910 $subArray = array_values($subArray);
911 $instring = "('".implode("', '",$subArray)."')";
912 $query = "select id, session_id, username from {mnet_session} where username in $instring";
913 $results = $DB->get_records_sql($query);
915 if ($results == false) {
916 // We seem to have a username that breaks our query:
917 // TODO: Handle this error appropriately
918 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
920 foreach($results as $emigrant) {
921 \core\session\manager
::touch_session($emigrant->session_id
);
926 $end = ob_end_clean();
928 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id
);
929 return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id
);
933 * Cron function will be called automatically by cron.php every 5 minutes
940 // run the keepalive client
941 $this->keepalive_client();
943 $random100 = rand(0,100);
944 if ($random100 < 10) { // Approximately 10% of the time.
945 // nuke olden sessions
946 $longtime = time() - (1 * 3600 * 24);
947 $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
952 * Cleanup any remote mnet_sessions, kill the local mnet_session data
954 * This is called by require_logout in moodlelib
958 function prelogout_hook() {
961 if (!is_enabled_auth('mnet')) {
965 // If the user is local to this Moodle:
966 if ($USER->mnethostid
== $this->mnet
->id
) {
967 $this->kill_children($USER->username
, sha1($_SERVER['HTTP_USER_AGENT']));
969 // Else the user has hit 'logout' at a Service Provider Moodle:
971 $this->kill_parent($USER->username
, sha1($_SERVER['HTTP_USER_AGENT']));
977 * The SP uses this function to kill the session on the parent IdP
979 * @param string $username Username for session to kill
980 * @param string $useragent SHA1 hash of user agent to look for
981 * @return string A plaintext report of what has happened
983 function kill_parent($username, $useragent) {
984 global $CFG, $USER, $DB;
986 require_once $CFG->dirroot
.'/mnet/xmlrpc/client.php';
997 $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid
));
999 $ignore = $DB->delete_records('mnet_session',
1000 array('username'=>$username,
1001 'useragent'=>$useragent,
1002 'mnethostid'=>$USER->mnethostid
));
1004 if (false != $mnetsessions) {
1005 $mnet_peer = new mnet_peer();
1006 $mnet_peer->set_id($USER->mnethostid
);
1008 $mnet_request = new mnet_xmlrpc_client();
1009 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1011 // set $token and $useragent parameters
1012 $mnet_request->add_param($username);
1013 $mnet_request->add_param($useragent);
1014 if ($mnet_request->send($mnet_peer) === false) {
1015 debugging(join("\n", $mnet_request->error
));
1024 * The IdP uses this function to kill child sessions on other hosts
1026 * @param string $username Username for session to kill
1027 * @param string $useragent SHA1 hash of user agent to look for
1028 * @return string A plaintext report of what has happened
1030 function kill_children($username, $useragent) {
1031 global $CFG, $USER, $DB;
1032 $remoteclient = null;
1033 if (defined('MNET_SERVER')) {
1034 $remoteclient = get_mnet_remote_client();
1036 require_once $CFG->dirroot
.'/mnet/xmlrpc/client.php';
1038 $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id
, 'username'=>$username));
1042 $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1044 if (false == $mnetsessions) {
1045 $returnstring .= "Could find no remote sessions\n";
1046 $mnetsessions = array();
1049 foreach($mnetsessions as $mnetsession) {
1050 // If this script is being executed by a remote peer, that means the user has clicked
1051 // logout on that peer, and the session on that peer can be deleted natively.
1053 if (isset($remoteclient->id
) && ($mnetsession->mnethostid
== $remoteclient->id
)) {
1056 $returnstring .= "Deleting session\n";
1058 $mnet_peer = new mnet_peer();
1059 $mnet_peer->set_id($mnetsession->mnethostid
);
1061 $mnet_request = new mnet_xmlrpc_client();
1062 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1064 // set $token and $useragent parameters
1065 $mnet_request->add_param($username);
1066 $mnet_request->add_param($useragent);
1067 if ($mnet_request->send($mnet_peer) === false) {
1068 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1069 join("\n", $mnet_request->error
));
1073 $ignore = $DB->delete_records('mnet_session',
1074 array('useragent'=>$useragent, 'userid'=>$userid));
1076 if (isset($remoteclient) && isset($remoteclient->id
)) {
1077 \core\session\manager
::kill_user_sessions($userid);
1079 return $returnstring;
1083 * When the IdP requests that child sessions are terminated,
1084 * this function will be called on each of the child hosts. The machine that
1085 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1087 * @param string $username Username for session to kill
1088 * @param string $useragent SHA1 hash of user agent to look for
1089 * @return bool True on success
1091 function kill_child($username, $useragent) {
1093 $remoteclient = get_mnet_remote_client();
1094 $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id
, 'useragent'=>$useragent));
1095 $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id
, 'useragent'=>$useragent));
1096 if (false != $session) {
1097 \core\session\manager
::kill_session($session->session_id
);
1104 * To delete a host, we must delete all current sessions that users from
1105 * that host are currently engaged in.
1107 * @param string $sessionidarray An array of session hashes
1108 * @return bool True on success
1110 function end_local_sessions(&$sessionArray) {
1112 if (is_array($sessionArray)) {
1113 while($session = array_pop($sessionArray)) {
1114 \core\session\manager
::kill_session($session->session_id
);
1122 * Returns the user's profile image info
1124 * If the user exists and has a profile picture, the returned array will contain keys:
1125 * f1 - the content of the default 100x100px image
1126 * f1_mimetype - the mimetype of the f1 file
1127 * f2 - the content of the 35x35px variant of the image
1128 * f2_mimetype - the mimetype of the f2 file
1130 * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1132 * @see process_new_icon()
1133 * @uses mnet_remote_client callable via MNet XML-RPC
1134 * @param int $userid The id of the user
1135 * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1137 function fetch_user_image($username) {
1140 if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id
))) {
1141 $fs = get_file_storage();
1142 $usercontext = context_user
::instance($user->id
, MUST_EXIST
);
1144 if ($f1 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.png')) {
1145 $return['f1'] = base64_encode($f1->get_content());
1146 $return['f1_mimetype'] = $f1->get_mimetype();
1147 } else if ($f1 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f1.jpg')) {
1148 $return['f1'] = base64_encode($f1->get_content());
1149 $return['f1_mimetype'] = $f1->get_mimetype();
1151 if ($f2 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f2.png')) {
1152 $return['f2'] = base64_encode($f2->get_content());
1153 $return['f2_mimetype'] = $f2->get_mimetype();
1154 } else if ($f2 = $fs->get_file($usercontext->id
, 'user', 'icon', 0, '/', 'f2.jpg')) {
1155 $return['f2'] = base64_encode($f2->get_content());
1156 $return['f2_mimetype'] = $f2->get_mimetype();
1164 * Returns the theme information and logo url as strings.
1166 * @return string The theme info
1168 function fetch_theme_info() {
1171 $themename = "$CFG->theme";
1172 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1174 $return['themename'] = $themename;
1175 $return['logourl'] = $logourl;
1180 * Determines if an MNET host is providing the nominated service.
1182 * @param int $mnethostid The id of the remote host
1183 * @param string $servicename The name of the service
1184 * @return bool Whether the service is available on the remote host
1186 function has_service($mnethostid, $servicename) {
1191 svc.id as serviceid,
1200 {mnet_host2service} h2s
1203 h.id = h2s.hostid AND
1205 h2s.serviceid = svc.id AND
1207 h2s.subscribe = '1'";
1209 return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1213 * Checks the MNET access control table to see if the username/mnethost
1214 * is permitted to login to this moodle.
1216 * @param string $username The username
1217 * @param int $mnethostid The id of the remote mnethost
1218 * @return bool Whether the user can login from the remote host
1220 function can_login_remotely($username, $mnethostid) {
1223 $accessctrl = 'allow';
1224 $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1225 if (!empty($aclrecord)) {
1226 $accessctrl = $aclrecord->accessctrl
;
1228 return $accessctrl == 'allow';
1231 function logoutpage_hook() {
1232 global $USER, $CFG, $redirect, $DB;
1234 if (!empty($USER->mnethostid
) and $USER->mnethostid
!= $CFG->mnet_localhost_id
) {
1235 $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid
));
1236 $redirect = $host->wwwroot
.'/';
1241 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1243 * @param object $logline The log information to be trimmed
1244 * @return object The passed logline object trimmed to not exceed storable limits
1246 function trim_logline ($logline) {
1247 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1249 foreach ($limits as $property => $limit) {
1250 if (isset($logline->$property)) {
1251 $logline->$property = substr($logline->$property, 0, $limit);
1259 * Returns a list of potential IdPs that this authentication plugin supports.
1260 * This is used to provide links on the login page.
1262 * @param string $wantsurl the relative url fragment the user wants to get to. You can use this to compose a returnurl, for example
1264 * @return array like:
1267 * 'url' => 'http://someurl',
1268 * 'icon' => new pix_icon(...),
1269 * 'name' => get_string('somename', 'auth_yourplugin'),
1273 function loginpage_idp_list($wantsurl) {
1276 // strip off wwwroot, since the remote site will prefix it's return url with this
1277 $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot
, '/') . '|' . preg_quote($CFG->httpswwwroot
, '/') . ')/', '', $wantsurl);
1279 $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1281 JOIN {mnet_host2service} m ON h.id = m.hostid
1282 JOIN {mnet_service} s ON s.id = m.serviceid
1283 JOIN {mnet_application} a ON h.applicationid = a.id
1284 WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1285 $params = array('sso_sp', 0, 1);
1287 if (!empty($CFG->mnet_all_hosts_id
)) {
1288 $sql .= " AND h.id <> ?";
1289 $params[] = $CFG->mnet_all_hosts_id
;
1292 if (!$hosts = $DB->get_records_sql($sql, $params)) {
1297 foreach ($hosts as $host) {
1299 'url' => new moodle_url($host->wwwroot
. $host->sso_jump_url
, array('hostwwwroot' => $CFG->wwwroot
, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1300 'icon' => new pix_icon('i/' . $host->application
. '_host', $host->name
),
1301 'name' => $host->name
,