Small upgrade to fix some guest->mnethostid. MDL-10375
[pfb-moodle.git] / auth / mnet / auth.php
blob30fa1c71e807f002407e8938ccb233cd363dfb76
1 <?php
3 /**
4 * @author Martin Dougiamas
5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
6 * @package moodle multiauth
8 * Authentication Plugin: Moodle Network Authentication
10 * Multiple host authentication support for Moodle Network.
12 * 2006-11-01 File created.
15 if (!defined('MOODLE_INTERNAL')) {
16 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
19 require_once($CFG->libdir.'/authlib.php');
21 /**
22 * Moodle Network authentication plugin.
24 class auth_plugin_mnet extends auth_plugin_base {
26 /**
27 * Constructor.
29 function auth_plugin_mnet() {
30 $this->authtype = 'mnet';
31 $this->config = get_config('auth/mnet');
34 /**
35 * Provides the allowed RPC services from this class as an array.
36 * @return array Allowed RPC services.
38 function mnet_publishes() {
40 $sso_idp = array();
41 $sso_idp['name'] = 'sso_idp'; // Name & Description go in lang file
42 $sso_idp['apiversion'] = 1;
43 $sso_idp['methods'] = array('user_authorise','keepalive_server', 'kill_children',
44 'refresh_log', 'fetch_user_image', 'fetch_theme_info',
45 'update_enrolments');
47 $sso_sp = array();
48 $sso_sp['name'] = 'sso_sp'; // Name & Description go in lang file
49 $sso_sp['apiversion'] = 1;
50 $sso_sp['methods'] = array('keepalive_client','kill_child');
52 return array($sso_idp, $sso_sp);
55 /**
56 * This function is normally used to determine if the username and password
57 * are correct for local logins. Always returns false, as local users do not
58 * need to login over mnet xmlrpc.
60 * @param string $username The username
61 * @param string $password The password
62 * @return bool Authentication success or failure.
64 function user_login($username, $password) {
65 return false; // error("Remote MNET users cannot login locally.");
68 /**
69 * Return user data for the provided token, compare with user_agent string.
71 * @param string $token The unique ID provided by remotehost.
72 * @param string $UA User Agent string.
73 * @return array $userdata Array of user info for remote host
75 function user_authorise($token, $useragent) {
76 global $CFG, $MNET, $SITE, $MNET_REMOTE_CLIENT;
77 require_once $CFG->dirroot . '/mnet/xmlrpc/server.php';
79 $mnet_session = get_record('mnet_session', 'token', $token, 'useragent', $useragent);
80 if (empty($mnet_session)) {
81 echo mnet_server_fault(1, get_string('authfail_nosessionexists', 'mnet'));
82 exit;
85 // check session confirm timeout
86 if ($mnet_session->confirm_timeout < time()) {
87 echo mnet_server_fault(2, get_string('authfail_sessiontimedout', 'mnet'));
88 exit;
91 // session okay, try getting the user
92 if (!$user = get_complete_user_data('id', $mnet_session->userid)) {
93 echo mnet_server_fault(3, get_string('authfail_usermismatch', 'mnet'));
94 exit;
97 $userdata = array();
98 $userdata['username'] = $user->username;
99 $userdata['email'] = $user->email;
100 $userdata['auth'] = 'mnet';
101 $userdata['confirmed'] = $user->confirmed;
102 $userdata['deleted'] = $user->deleted;
103 $userdata['firstname'] = $user->firstname;
104 $userdata['lastname'] = $user->lastname;
105 $userdata['city'] = $user->city;
106 $userdata['country'] = $user->country;
107 $userdata['lang'] = $user->lang;
108 $userdata['timezone'] = $user->timezone;
109 $userdata['description'] = $user->description;
110 $userdata['mailformat'] = $user->mailformat;
111 $userdata['maildigest'] = $user->maildigest;
112 $userdata['maildisplay'] = $user->maildisplay;
113 $userdata['htmleditor'] = $user->htmleditor;
114 $userdata['wwwroot'] = $MNET->wwwroot;
115 $userdata['session.gc_maxlifetime'] = ini_get('session.gc_maxlifetime');
116 $userdata['picture'] = $user->picture;
117 if (!empty($user->picture)) {
118 $imagefile = "{$CFG->dataroot}/users/{$user->id}/f1.jpg";
119 if (file_exists($imagefile)) {
120 $userdata['imagehash'] = sha1(file_get_contents($imagefile));
124 $userdata['myhosts'] = array();
125 if($courses = get_my_courses($user->id, 'id', 'id, visible')) {
126 $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
129 $sql = "
130 SELECT
131 h.name as hostname,
132 h.wwwroot,
133 h.id as hostid,
134 count(c.id) as count
135 FROM
136 {$CFG->prefix}mnet_enrol_course c,
137 {$CFG->prefix}mnet_enrol_assignments a,
138 {$CFG->prefix}mnet_host h
139 WHERE
140 c.id = a.courseid AND
141 c.hostid = h.id AND
142 a.userid = '{$user->id}' AND
143 c.hostid != '{$MNET_REMOTE_CLIENT->id}'
144 GROUP BY
145 h.name,
146 h.id,
147 h.wwwroot";
148 if ($courses = get_records_sql($sql)) {
149 foreach($courses as $course) {
150 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
154 return $userdata;
158 * Generate a random string for use as an RPC session token.
160 function generate_token() {
161 return sha1(str_shuffle('' . mt_rand() . time()));
165 * Starts an RPC jump session and returns the jump redirect URL.
167 function start_jump_session($mnethostid, $wantsurl) {
168 global $CFG;
169 global $USER;
170 global $MNET;
171 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
173 // check remote login permissions
174 if (! has_capability('moodle/site:mnetlogintoremote', get_context_instance(CONTEXT_SYSTEM, SITEID))
175 or is_mnet_remote_user($USER)
176 or $USER->username == 'guest'
177 or empty($USER->id)) {
178 error(get_string('notpermittedtojump', 'mnet'));
181 // check for SSO publish permission first
182 if ($this->has_service($mnethostid, 'sso_sp') == false) {
183 error(get_string('hostnotconfiguredforsso', 'mnet'));
186 // set RPC timeout to 30 seconds if not configured
187 // TODO: Is this needed/useful/problematic?
188 if (empty($this->config->rpc_negotiation_timeout)) {
189 set_config('rpc_negotiation_timeout', '30', 'auth/mnet');
192 // get the host info
193 $mnet_peer = new mnet_peer();
194 $mnet_peer->set_id($mnethostid);
196 // set up the session
197 $mnet_session = get_record('mnet_session',
198 'userid', $USER->id,
199 'mnethostid', $mnethostid,
200 'useragent', sha1($_SERVER['HTTP_USER_AGENT']));
201 if ($mnet_session == false) {
202 $mnet_session = new object();
203 $mnet_session->mnethostid = $mnethostid;
204 $mnet_session->userid = $USER->id;
205 $mnet_session->username = $USER->username;
206 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
207 $mnet_session->token = $this->generate_token();
208 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
209 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
210 $mnet_session->session_id = session_id();
211 if (! $mnet_session->id = insert_record('mnet_session', addslashes_object($mnet_session))) {
212 error(get_string('databaseerror', 'mnet'));
214 } else {
215 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
216 $mnet_session->token = $this->generate_token();
217 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
218 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
219 $mnet_session->session_id = session_id();
220 if (false == update_record('mnet_session', addslashes_object($mnet_session))) {
221 error(get_string('databaseerror', 'mnet'));
225 // construct the redirection URL
226 //$transport = mnet_get_protocol($mnet_peer->transport);
227 $wantsurl = urlencode($wantsurl);
228 $url = "{$mnet_peer->wwwroot}/auth/mnet/land.php?token={$mnet_session->token}&idp={$MNET->wwwroot}&wantsurl={$wantsurl}";
230 return $url;
234 * This function confirms the remote (ID provider) host's mnet session
235 * by communicating the token and UA over the XMLRPC transport layer, and
236 * returns the local user record on success.
238 * @param string $token The random session token.
239 * @param string $remotewwwroot The ID provider wwwroot.
240 * @return array The local user record.
242 function confirm_mnet_session($token, $remotewwwroot) {
243 global $CFG, $MNET, $SESSION;
244 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
246 // verify the remote host is configured locally before attempting RPC call
247 if (! $remotehost = get_record('mnet_host', 'wwwroot', $remotewwwroot)) {
248 error(get_string('notpermittedtoland', 'mnet'));
251 // get the originating (ID provider) host info
252 $remotepeer = new mnet_peer();
253 $remotepeer->set_wwwroot($remotewwwroot);
255 // set up the RPC request
256 $mnetrequest = new mnet_xmlrpc_client();
257 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
259 // set $token and $useragent parameters
260 $mnetrequest->add_param($token);
261 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
263 // Thunderbirds are go! Do RPC call and store response
264 if ($mnetrequest->send($remotepeer) === true) {
265 $remoteuser = (object) $mnetrequest->response;
266 } else {
267 foreach ($mnetrequest->error as $errormessage) {
268 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
269 if($code == 702) {
270 $site = get_site();
271 print_error('mnet_session_prohibited','mnet', $remotewwwroot, format_string($site->fullname));
272 exit;
274 $message .= "ERROR $code:<br/>$errormessage<br/>";
276 error("RPC auth/mnet/user_authorise:<br/>$message");
278 unset($mnetrequest);
280 if (empty($remoteuser) or empty($remoteuser->username)) {
281 print_error('unknownerror', 'mnet');
282 exit;
285 $firsttime = false;
287 // get the local record for the remote user
288 $localuser = get_record('user', 'username', $remoteuser->username, 'mnethostid', $remotehost->id);
290 // add the remote user to the database if necessary, and if allowed
291 // TODO: refactor into a separate function
292 if (! $localuser->id) {
293 if (empty($this->config->auto_add_remote_users)) {
294 error(get_string('nolocaluser', 'mnet'));
296 $remoteuser->mnethostid = $remotehost->id;
297 if (! insert_record('user', addslashes_object($remoteuser))) {
298 error(get_string('databaseerror', 'mnet'));
300 $firsttime = true;
301 if (! $localuser = get_record('user', 'username', addslashes($remoteuser->username), 'mnethostid', $remotehost->id)) {
302 error(get_string('nolocaluser', 'mnet'));
306 // check sso access control list for permission first
307 if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
308 print_error('sso_mnet_login_refused', 'mnet', '', array($localuser->username, $remotehost->name));
311 $session_gc_maxlifetime = 1440;
313 // update the local user record with remote user data
314 foreach ((array) $remoteuser as $key => $val) {
315 if ($key == 'session.gc_maxlifetime') {
316 $session_gc_maxlifetime = $val;
317 continue;
320 // TODO: fetch image if it has changed
321 if ($key == 'imagehash') {
322 $dirname = "{$CFG->dataroot}/users/{$localuser->id}";
323 $filename = "$dirname/f1.jpg";
325 $localhash = '';
326 if (file_exists($filename)) {
327 $localhash = sha1(file_get_contents($filename));
328 } elseif (!file_exists($dirname)) {
329 mkdir($dirname);
332 if ($localhash != $val) {
333 // fetch image from remote host
334 $fetchrequest = new mnet_xmlrpc_client();
335 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
336 $fetchrequest->add_param($localuser->username);
337 if ($fetchrequest->send($remotepeer) === true) {
338 if (strlen($fetchrequest->response['f1']) > 0) {
339 $imagecontents = base64_decode($fetchrequest->response['f1']);
340 file_put_contents($filename, $imagecontents);
342 if (strlen($fetchrequest->response['f2']) > 0) {
343 $imagecontents = base64_decode($fetchrequest->response['f2']);
344 file_put_contents($dirname.'/f2.jpg', $imagecontents);
350 if($key == 'myhosts') {
351 $localuser->mnet_foreign_host_array = array();
352 foreach($val as $rhost) {
353 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
354 $url = clean_param($rhost['url'], PARAM_URL);
355 $count = clean_param($rhost['count'], PARAM_INT);
356 $url_is_local = stristr($url , $CFG->wwwroot);
357 if (!empty($name) && !empty($count) && empty($url_is_local)) {
358 $localuser->mnet_foreign_host_array[] = array('name' => $name,
359 'url' => $url,
360 'count' => $count);
365 $localuser->{$key} = $val;
368 $localuser->mnethostid = $remotepeer->id;
370 $bool = update_record('user', addslashes_object($localuser));
371 if (!$bool) {
372 // TODO: Jonathan to clean up mess
373 // Actually, this should never happen (modulo race conditions) - ML
374 error("updating user failed in mnet/auth/confirm_mnet_session ");
377 // set up the session
378 $mnet_session = get_record('mnet_session',
379 'userid', $localuser->id,
380 'mnethostid', $remotepeer->id,
381 'useragent', sha1($_SERVER['HTTP_USER_AGENT']));
382 if ($mnet_session == false) {
383 $mnet_session = new object();
384 $mnet_session->mnethostid = $remotepeer->id;
385 $mnet_session->userid = $localuser->id;
386 $mnet_session->username = $localuser->username;
387 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
388 $mnet_session->token = $token; // Needed to support simultaneous sessions
389 // and preserving DB rec uniqueness
390 $mnet_session->confirm_timeout = time();
391 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
392 $mnet_session->session_id = session_id();
393 if (! $mnet_session->id = insert_record('mnet_session', addslashes_object($mnet_session))) {
394 error(get_string('databaseerror', 'mnet'));
396 } else {
397 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
398 update_record('mnet_session', addslashes_object($mnet_session));
401 if (!$firsttime) {
402 // repeat customer! let the IDP know about enrolments
403 // we have for this user.
404 // set up the RPC request
405 $mnetrequest = new mnet_xmlrpc_client();
406 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
408 // pass username and an assoc array of "my courses"
409 // with info so that the IDP can maintain mnet_enrol_assignments
410 $mnetrequest->add_param($remoteuser->username);
411 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary,
412 startdate, cost, currency, defaultrole, visible';
413 $courses = get_my_courses($localuser->id, 'visible DESC,sortorder ASC', $fields);
414 if (is_array($courses) && !empty($courses)) {
415 // Second request to do the JOINs that we'd have done
416 // inside get_my_courses() if we had been allowed
417 $sql = "SELECT c.id,
418 cc.name AS cat_name, cc.description AS cat_description,
419 r.shortname as defaultrolename
420 FROM {$CFG->prefix}course c
421 JOIN {$CFG->prefix}course_categories cc ON c.category = cc.id
422 LEFT OUTER JOIN {$CFG->prefix}role r ON c.defaultrole = r.id
423 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
424 $extra = get_records_sql($sql);
426 $keys = array_keys($courses);
427 $defaultrolename = get_field('role', 'shortname', 'id', $CFG->defaultcourseroleid);
428 foreach ($keys AS $id) {
429 if ($courses[$id]->visible == 0) {
430 unset($courses[$id]);
431 continue;
433 $courses[$id]->cat_id = $courses[$id]->category;
434 $courses[$id]->defaultroleid = $courses[$id]->defaultrole;
435 unset($courses[$id]->category);
436 unset($courses[$id]->defaultrole);
437 unset($courses[$id]->visible);
439 $courses[$id]->cat_name = $extra[$id]->cat_name;
440 $courses[$id]->cat_description = $extra[$id]->cat_description;
441 if (!empty($extra[$id]->defaultrolename)) {
442 $courses[$id]->defaultrolename = $extra[$id]->defaultrolename;
443 } else {
444 $courses[$id]->defaultrolename = $defaultrolename;
446 // coerce to array
447 $courses[$id] = (array)$courses[$id];
449 } else {
450 // if the array is empty, send it anyway
451 // we may be clearing out stale entries
452 $courses = array();
454 $mnetrequest->add_param($courses);
456 // Call 0800-RPC Now! -- we don't care too much if it fails
457 // as it's just informational.
458 if ($mnetrequest->send($remotepeer) === false) {
459 // error_log(print_r($mnetrequest->error,1));
463 return $localuser;
467 * Invoke this function _on_ the IDP to update it with enrolment info local to
468 * the SP right after calling user_authorise()
470 * Normally called by the SP after calling
472 * @param string $username The username
473 * @param string $courses Assoc array of courses following the structure of mnet_enrol_course
474 * @return bool
476 function update_enrolments($username, $courses) {
477 global $MNET_REMOTE_CLIENT, $CFG;
479 if (empty($username) || !is_array($courses)) {
480 return false;
482 // make sure it is a user we have an in active session
483 // with that host...
484 $userid = get_field('mnet_session', 'userid',
485 'username', addslashes($username),
486 'mnethostid', (int)$MNET_REMOTE_CLIENT->id);
487 if (!$userid) {
488 return false;
491 if (empty($courses)) { // no courses? clear out quickly
492 delete_records('mnet_enrol_assignments',
493 'hostid', (int)$MNET_REMOTE_CLIENT->id,
494 'userid', $userid);
495 return true;
498 // IMPORTANT: Ask for remoteid as the first element in the query, so
499 // that the array that comes back is indexed on the same field as the
500 // array that we have received from the remote client
501 $sql = '
502 SELECT
503 c.remoteid,
504 c.id,
505 c.cat_id,
506 c.cat_name,
507 c.cat_description,
508 c.sortorder,
509 c.fullname,
510 c.shortname,
511 c.idnumber,
512 c.summary,
513 c.startdate,
514 c.cost,
515 c.currency,
516 c.defaultroleid,
517 c.defaultrolename,
518 a.id as assignmentid
519 FROM
520 '.$CFG->prefix.'mnet_enrol_course c
521 LEFT JOIN
522 '.$CFG->prefix.'mnet_enrol_assignments a
524 (a.courseid = c.id AND
525 a.hostid = c.hostid AND
526 a.userid = \''.$userid.'\')
527 WHERE
528 c.hostid = \''.(int)$MNET_REMOTE_CLIENT->id.'\'';
530 $currentcourses = get_records_sql($sql);
532 $local_courseid_array = array();
533 foreach($courses as $course) {
535 $course['remoteid'] = $course['id'];
536 $course['hostid'] = (int)$MNET_REMOTE_CLIENT->id;
537 $userisregd = false;
539 // First up - do we have a record for this course?
540 if (!array_key_exists($course['remoteid'], $currentcourses)) {
541 // No record - we must create it
542 $course['id'] = insert_record('mnet_enrol_course', addslashes_object((object)$course));
543 $currentcourse = (object)$course;
544 } else {
545 // Pointer to current course:
546 $currentcourse =& $currentcourses[$course['remoteid']];
547 // We have a record - is it up-to-date?
548 $course['id'] = $currentcourse->id;
550 $saveflag = false;
552 foreach($course as $key => $value) {
553 if ($currentcourse->$key != $value) {
554 $saveflag = true;
555 $currentcourse->$key = $value;
559 if ($saveflag) {
560 update_record('mnet_enrol_course', addslashes_object($currentcourse));
563 if (isset($currentcourse->assignmentid) && is_numeric($currentcourse->assignmentid)) {
564 $userisregd = true;
568 // By this point, we should always have a $dataObj->id
569 $local_courseid_array[] = $course['id'];
571 // Do we have a record for this assignment?
572 if ($userisregd) {
573 // Yes - we know about this one already
574 // We don't want to do updates because the new data is probably
575 // 'less complete' than the data we have.
576 } else {
577 // No - create a record
578 $assignObj = new stdClass();
579 $assignObj->userid = $userid;
580 $assignObj->hostid = (int)$MNET_REMOTE_CLIENT->id;
581 $assignObj->courseid = $course['id'];
582 $assignObj->rolename = $course['defaultrolename'];
583 $assignObj->id = insert_record('mnet_enrol_assignments', addslashes_object($assignObj));
587 // Clean up courses that the user is no longer enrolled in.
588 $local_courseid_string = implode(', ', $local_courseid_array);
589 $whereclause = " userid = '$userid' AND hostid = '{$MNET_REMOTE_CLIENT->id}' AND courseid NOT IN ($local_courseid_string)";
590 delete_records_select('mnet_enrol_assignments', $whereclause);
594 * Returns true if this authentication plugin is 'internal'.
596 * @return bool
598 function is_internal() {
599 return false;
603 * Returns true if this authentication plugin can change the user's
604 * password.
606 * @return bool
608 function can_change_password() {
609 //TODO: it should be able to redirect, right?
610 return false;
614 * Returns the URL for changing the user's pw, or false if the default can
615 * be used.
617 * @return string
619 function change_password_url() {
620 return '';
624 * Prints a form for configuring this authentication plugin.
626 * This function is called from admin/auth.php, and outputs a full page with
627 * a form for configuring this plugin.
629 * @param array $page An object containing all the data for this page.
631 function config_form($config, $err, $user_fields) {
632 global $CFG;
634 $query = "
635 SELECT
636 h.id,
637 h.name as hostname,
638 h.wwwroot,
639 h2idp.publish as idppublish,
640 h2idp.subscribe as idpsubscribe,
641 idp.name as idpname,
642 h2sp.publish as sppublish,
643 h2sp.subscribe as spsubscribe,
644 sp.name as spname
645 FROM
646 {$CFG->prefix}mnet_host h
647 LEFT JOIN
648 {$CFG->prefix}mnet_host2service h2idp
650 (h.id = h2idp.hostid AND
651 (h2idp.publish = 1 OR
652 h2idp.subscribe = 1))
653 INNER JOIN
654 {$CFG->prefix}mnet_service idp
656 (h2idp.serviceid = idp.id AND
657 idp.name = 'sso_idp')
658 LEFT JOIN
659 {$CFG->prefix}mnet_host2service h2sp
661 (h.id = h2sp.hostid AND
662 (h2sp.publish = 1 OR
663 h2sp.subscribe = 1))
664 INNER JOIN
665 {$CFG->prefix}mnet_service sp
667 (h2sp.serviceid = sp.id AND
668 sp.name = 'sso_sp')
669 WHERE
670 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
671 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
672 h.id != {$CFG->mnet_localhost_id}
673 ORDER BY
674 h.name ASC";
676 $id_providers = array();
677 $service_providers = array();
678 if ($resultset = get_records_sql($query)) {
679 foreach($resultset as $hostservice) {
680 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
681 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
683 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
684 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
689 include "config.html";
693 * Processes and stores configuration data for this authentication plugin.
695 function process_config($config) {
696 // set to defaults if undefined
697 if (!isset ($config->rpc_negotiation_timeout)) {
698 $config->rpc_negotiation_timeout = '30';
700 if (!isset ($config->auto_add_remote_users)) {
701 $config->auto_add_remote_users = '0';
704 // save settings
705 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth/mnet');
706 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth/mnet');
708 return true;
712 * Poll the IdP server to let it know that a user it has authenticated is still
713 * online
715 * @return void
717 function keepalive_client() {
718 global $CFG, $MNET;
719 $cutoff = time() - 300; // TODO - find out what the remote server's session
720 // cutoff is, and preempt that
722 $sql = "
723 select
725 username,
726 mnethostid
727 from
728 {$CFG->prefix}user
729 where
730 lastaccess > '$cutoff' AND
731 mnethostid != '{$CFG->mnet_localhost_id}'
732 order by
733 mnethostid";
735 $immigrants = get_records_sql($sql);
737 if ($immigrants == false) {
738 return true;
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");
760 continue;
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.");
767 continue;
769 } else {
770 debugging("Server side error has occured on host $mnethostid: " .
771 join("\n", $mnet_request->error));
772 break;
775 $query = "SELECT
776 l.id as remoteid,
777 l.time,
778 l.userid,
779 l.ip,
780 l.course,
781 l.module,
782 l.cmid,
783 l.action,
784 l.url,
785 l.info,
786 c.fullname as coursename,
787 c.modinfo as modinfo,
788 u.username
789 FROM
790 {$CFG->prefix}user u,
791 {$CFG->prefix}log l,
792 {$CFG->prefix}course c
793 WHERE
794 l.userid = u.id AND
795 u.mnethostid = '$mnethostid' AND
796 l.id > '".$mnet_request->response['last log id']."' AND
797 c.id = l.course
798 ORDER BY
799 remoteid ASC";
801 $results = get_records_sql($query);
803 if (false == $results) continue;
805 $param = array();
807 foreach($results as $result) {
808 if (!empty($result->modinfo) && !empty($result->cmid)) {
809 $modinfo = unserialize($result->modinfo);
810 unset($result->modinfo);
811 $modulearray = array();
812 foreach($modinfo as $module) {
813 $modulearray[$module->cm] = urldecode($module->name);
815 $result->resource_name = $modulearray[$result->cmid];
816 } else {
817 $result->resource_name = '';
820 $param[] = array (
821 'remoteid' => $result->remoteid,
822 'time' => $result->time,
823 'userid' => $result->userid,
824 'ip' => $result->ip,
825 'course' => $result->course,
826 'coursename' => $result->coursename,
827 'module' => $result->module,
828 'cmid' => $result->cmid,
829 'action' => $result->action,
830 'url' => $result->url,
831 'info' => $result->info,
832 'resource_name' => $result->resource_name,
833 'username' => $result->username
837 unset($result);
839 $mnet_request = new mnet_xmlrpc_client();
840 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
842 // set $token and $useragent parameters
843 $mnet_request->add_param($param);
845 if ($mnet_request->send($mnet_peer) === true) {
846 if ($mnet_request->response['code'] > 0) {
847 debugging($mnet_request->response['message']);
849 } else {
850 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
856 * Receives an array of log entries from an SP and adds them to the mnet_log
857 * table
859 * @param array $array An array of usernames
860 * @return string "All ok" or an error message
862 function refresh_log($array) {
863 global $CFG, $MNET_REMOTE_CLIENT;
865 // We don't want to output anything to the client machine
866 $start = ob_start();
868 $returnString = '';
869 begin_sql();
870 $useridarray = array();
872 foreach($array as $logEntry) {
873 $logEntryObj = (object)$logEntry;
874 $logEntryObj->hostid = $MNET_REMOTE_CLIENT->id;
876 if (isset($useridarray[$logEntryObj->username])) {
877 $logEntryObj->userid = $useridarray[$logEntryObj->username];
878 } else {
879 $logEntryObj->userid = get_field('user','id','username',$logEntryObj->username);
880 if ($logEntryObj->userid == false) {
881 $logEntryObj->userid = 0;
883 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
886 unset($logEntryObj->username);
888 $insertok = insert_record('mnet_log', addslashes_object($logEntryObj), false);
890 if ($insertok) {
891 $MNET_REMOTE_CLIENT->last_log_id = $logEntryObj->remoteid;
892 } else {
893 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
896 $MNET_REMOTE_CLIENT->commit();
897 commit_sql();
899 $end = ob_end_clean();
901 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
902 return array('code' => 1, 'message' => $returnString);
906 * Receives an array of usernames from a remote machine and prods their
907 * sessions to keep them alive
909 * @param array $array An array of usernames
910 * @return string "All ok" or an error message
912 function keepalive_server($array) {
913 global $MNET_REMOTE_CLIENT, $CFG;
915 $CFG->usesid = true;
916 // Addslashes to all usernames, so we can build the query string real
917 // simply with 'implode'
918 $array = array_map('addslashes', $array);
920 // We don't want to output anything to the client machine
921 $start = ob_start();
923 // We'll get session records in batches of 30
924 $superArray = array_chunk($array, 30);
926 $returnString = '';
928 foreach($superArray as $subArray) {
929 $subArray = array_values($subArray);
930 $instring = "('".implode("', '",$subArray)."')";
931 $query = "select id, session_id, username from {$CFG->prefix}mnet_session where username in $instring";
932 $results = get_records_sql($query);
934 if ($results == false) {
935 // We seem to have a username that breaks our query:
936 // TODO: Handle this error appropriately
937 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
938 } else {
939 // TODO: This process of killing and re-starting the session
940 // will cause PHP to forget any custom session_set_save_handler
941 // stuff. Subsequent attempts to prod existing sessions will
942 // fail, because PHP will look in wherever the default place
943 // may be (files?) and probably create a new session with the
944 // right session ID in that location. If it doesn't have write-
945 // access to that location, then it will fail... not sure how
946 // apparent that will be.
947 // There is no way to capture what the custom session handler
948 // is and then reset it on each pass - I checked that out
949 // already.
950 $sesscache = clone($_SESSION);
951 $sessidcache = session_id();
952 session_write_close();
953 unset($_SESSION);
955 $uc = ini_get('session.use_cookies');
956 ini_set('session.use_cookies', false);
957 foreach($results as $emigrant) {
959 unset($_SESSION);
960 session_name('MoodleSession'.$CFG->sessioncookie);
961 session_id($emigrant->session_id);
962 session_start();
963 session_write_close();
966 ini_set('session.use_cookies', $uc);
967 session_name('MoodleSession'.$CFG->sessioncookie);
968 session_id($sessidcache);
969 session_start();
970 $_SESSION = clone($sesscache);
971 session_write_close();
975 $end = ob_end_clean();
977 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
978 return array('code' => 1, 'message' => $returnString, 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
982 * Cron function will be called automatically by cron.php every 5 minutes
984 * @return void
986 function cron() {
988 // run the keepalive client
989 $this->keepalive_client();
991 // admin/cron.php should have run srand for us
992 $random100 = rand(0,100);
993 if ($random100 < 10) { // Approximately 10% of the time.
994 // nuke olden sessions
995 $longtime = time() - (1 * 3600 * 24);
996 delete_records_select('mnet_session', "expires < $longtime");
1001 * Cleanup any remote mnet_sessions, kill the local mnet_session data
1003 * This is called by require_logout in moodlelib
1005 * @return void
1007 function prelogout_hook() {
1008 global $MNET, $CFG, $USER;
1009 if (!is_enabled_auth('mnet')) {
1010 return;
1013 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1015 // If the user is local to this Moodle:
1016 if ($USER->mnethostid == $MNET->id) {
1017 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1019 // Else the user has hit 'logout' at a Service Provider Moodle:
1020 } else {
1021 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1027 * The SP uses this function to kill the session on the parent IdP
1029 * @param string $username Username for session to kill
1030 * @param string $useragent SHA1 hash of user agent to look for
1031 * @return string A plaintext report of what has happened
1033 function kill_parent($username, $useragent) {
1034 global $CFG, $USER;
1035 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1036 $sql = "
1037 select
1039 from
1040 {$CFG->prefix}mnet_session s
1041 where
1042 s.username = '".addslashes($username)."' AND
1043 s.useragent = '$useragent' AND
1044 s.mnethostid = '{$USER->mnethostid}'";
1046 $mnetsessions = get_records_sql($sql);
1048 $ignore = delete_records('mnet_session',
1049 'username', addslashes($username),
1050 'useragent', $useragent,
1051 'mnethostid', $USER->mnethostid);
1053 if (false != $mnetsessions) {
1054 $mnet_peer = new mnet_peer();
1055 $mnet_peer->set_id($USER->mnethostid);
1057 $mnet_request = new mnet_xmlrpc_client();
1058 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1060 // set $token and $useragent parameters
1061 $mnet_request->add_param($username);
1062 $mnet_request->add_param($useragent);
1063 if ($mnet_request->send($mnet_peer) === false) {
1064 debugging(join("\n", $mnet_request->error));
1065 return false;
1069 $_SESSION = array();
1070 return true;
1074 * The IdP uses this function to kill child sessions on other hosts
1076 * @param string $username Username for session to kill
1077 * @param string $useragent SHA1 hash of user agent to look for
1078 * @return string A plaintext report of what has happened
1080 function kill_children($username, $useragent) {
1081 global $CFG, $USER, $MNET_REMOTE_CLIENT;
1082 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1084 $userid = get_field('user', 'id', 'mnethostid', $CFG->mnet_localhost_id, 'username', addslashes($username));
1086 $returnstring = '';
1087 $sql = "
1088 select
1090 from
1091 {$CFG->prefix}mnet_session s
1092 where
1093 s.userid = '{$userid}' AND
1094 s.useragent = '{$useragent}'";
1096 // If we are being executed from a remote machine (client) we don't have
1097 // to kill the moodle session on that machine.
1098 if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1099 $excludeid = $MNET_REMOTE_CLIENT->id;
1100 } else {
1101 $excludeid = -1;
1104 $mnetsessions = get_records_sql($sql);
1106 if (false == $mnetsessions) {
1107 $returnstring .= "Could find no remote sessions\n$sql\n";
1108 $mnetsessions = array();
1111 foreach($mnetsessions as $mnetsession) {
1112 $returnstring .= "Deleting session\n";
1114 if ($mnetsession->mnethostid == $excludeid) continue;
1116 $mnet_peer = new mnet_peer();
1117 $mnet_peer->set_id($mnetsession->mnethostid);
1119 $mnet_request = new mnet_xmlrpc_client();
1120 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1122 // set $token and $useragent parameters
1123 $mnet_request->add_param($username);
1124 $mnet_request->add_param($useragent);
1125 if ($mnet_request->send($mnet_peer) === false) {
1126 debugging("Server side error has occured on host $mnethostid: " .
1127 join("\n", $mnet_request->error));
1131 $ignore = delete_records('mnet_session',
1132 'useragent', $useragent,
1133 'userid', $userid);
1135 if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1136 $start = ob_start();
1138 $uc = ini_get('session.use_cookies');
1139 ini_set('session.use_cookies', false);
1140 $sesscache = clone($_SESSION);
1141 $sessidcache = session_id();
1142 session_write_close();
1143 unset($_SESSION);
1146 session_id($mnetsession->session_id);
1147 session_start();
1148 session_unregister("USER");
1149 session_unregister("SESSION");
1150 unset($_SESSION);
1151 $_SESSION = array();
1152 session_write_close();
1155 ini_set('session.use_cookies', $uc);
1156 session_name('MoodleSession'.$CFG->sessioncookie);
1157 session_id($sessidcache);
1158 session_start();
1159 $_SESSION = clone($sesscache);
1160 session_write_close();
1162 $end = ob_end_clean();
1163 } else {
1164 $_SESSION = array();
1166 return $returnstring;
1170 * TODO:Untested When the IdP requests that child sessions are terminated,
1171 * this function will be called on each of the child hosts. The machine that
1172 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1174 * @param string $username Username for session to kill
1175 * @param string $useragent SHA1 hash of user agent to look for
1176 * @return bool True on success
1178 function kill_child($username, $useragent) {
1179 global $CFG, $MNET_REMOTE_CLIENT;
1180 $session = get_record('mnet_session', 'username', addslashes($username), 'mnethostid', $MNET_REMOTE_CLIENT->id, 'useragent', $useragent);
1181 if (false != $session) {
1182 $start = ob_start();
1184 $uc = ini_get('session.use_cookies');
1185 ini_set('session.use_cookies', false);
1186 $sesscache = clone($_SESSION);
1187 $sessidcache = session_id();
1188 session_write_close();
1189 unset($_SESSION);
1192 session_id($session->session_id);
1193 session_start();
1194 session_unregister("USER");
1195 session_unregister("SESSION");
1196 unset($_SESSION);
1197 $_SESSION = array();
1198 session_write_close();
1201 ini_set('session.use_cookies', $uc);
1202 session_name('MoodleSession'.$CFG->sessioncookie);
1203 session_id($sessidcache);
1204 session_start();
1205 $_SESSION = clone($sesscache);
1206 session_write_close();
1208 $end = ob_end_clean();
1209 return true;
1211 return false;
1215 * To delete a host, we must delete all current sessions that users from
1216 * that host are currently engaged in.
1218 * @param string $sessionidarray An array of session hashes
1219 * @return bool True on success
1221 function end_local_sessions(&$sessionArray) {
1222 global $CFG;
1223 if (is_array($sessionArray)) {
1224 $start = ob_start();
1226 $uc = ini_get('session.use_cookies');
1227 ini_set('session.use_cookies', false);
1228 $sesscache = clone($_SESSION);
1229 $sessidcache = session_id();
1230 session_write_close();
1231 unset($_SESSION);
1233 while($session = array_pop($sessionArray)) {
1234 session_id($session->session_id);
1235 session_start();
1236 session_unregister("USER");
1237 session_unregister("SESSION");
1238 unset($_SESSION);
1239 $_SESSION = array();
1240 session_write_close();
1243 ini_set('session.use_cookies', $uc);
1244 session_name('MoodleSession'.$CFG->sessioncookie);
1245 session_id($sessidcache);
1246 session_start();
1247 $_SESSION = clone($sesscache);
1249 $end = ob_end_clean();
1250 return true;
1252 return false;
1256 * Returns the user's image as a base64 encoded string.
1258 * @param int $userid The id of the user
1259 * @return string The encoded image
1261 function fetch_user_image($username) {
1262 global $CFG;
1264 if ($user = get_record('user', 'username', addslashes($username), 'mnethostid', $CFG->mnet_localhost_id)) {
1265 $filename1 = "{$CFG->dataroot}/users/{$user->id}/f1.jpg";
1266 $filename2 = "{$CFG->dataroot}/users/{$user->id}/f2.jpg";
1267 $return = array();
1268 if (file_exists($filename1)) {
1269 $return['f1'] = base64_encode(file_get_contents($filename1));
1271 if (file_exists($filename2)) {
1272 $return['f2'] = base64_encode(file_get_contents($filename2));
1274 return $return;
1276 return false;
1280 * Returns the theme information and logo url as strings.
1282 * @return string The theme info
1284 function fetch_theme_info() {
1285 global $CFG;
1287 $themename = "$CFG->theme";
1288 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1290 $return['themename'] = $themename;
1291 $return['logourl'] = $logourl;
1292 return $return;
1296 * Determines if an MNET host is providing the nominated service.
1298 * @param int $mnethostid The id of the remote host
1299 * @param string $servicename The name of the service
1300 * @return bool Whether the service is available on the remote host
1302 function has_service($mnethostid, $servicename) {
1303 global $CFG;
1305 $sql = "
1306 SELECT
1307 svc.id as serviceid,
1308 svc.name,
1309 svc.description,
1310 svc.offer,
1311 svc.apiversion,
1312 h2s.id as h2s_id
1313 FROM
1314 {$CFG->prefix}mnet_service svc,
1315 {$CFG->prefix}mnet_host2service h2s
1316 WHERE
1317 h2s.hostid = '$mnethostid' AND
1318 h2s.serviceid = svc.id AND
1319 svc.name = '$servicename' AND
1320 h2s.subscribe = '1'";
1322 return get_records_sql($sql);
1326 * Checks the MNET access control table to see if the username/mnethost
1327 * is permitted to login to this moodle.
1329 * @param string $username The username
1330 * @param int $mnethostid The id of the remote mnethost
1331 * @return bool Whether the user can login from the remote host
1333 function can_login_remotely($username, $mnethostid) {
1334 $accessctrl = 'allow';
1335 $aclrecord = get_record('mnet_sso_access_control', 'username', addslashes($username), 'mnet_host_id', $mnethostid);
1336 if (!empty($aclrecord)) {
1337 $accessctrl = $aclrecord->accessctrl;
1339 return $accessctrl == 'allow';
1342 function logoutpage_hook() {
1343 global $USER, $CFG, $redirect;
1345 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1346 $host = get_record('mnet_host', 'id', $USER->mnethostid);
1347 $redirect = $host->wwwroot.'/';