Issue:
[moodle-pu.git] / auth / mnet / auth.php
blob3f611062f1e0a8af67a462a42430cbf8cab3cdd6
1 <?php // $Id$
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}{$mnet_peer->application->sso_land_url}?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', addslashes($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);
341 $localuser->picture = 1;
343 if (strlen($fetchrequest->response['f2']) > 0) {
344 $imagecontents = base64_decode($fetchrequest->response['f2']);
345 file_put_contents($dirname.'/f2.jpg', $imagecontents);
351 if($key == 'myhosts') {
352 $localuser->mnet_foreign_host_array = array();
353 foreach($val as $rhost) {
354 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
355 $url = clean_param($rhost['url'], PARAM_URL);
356 $count = clean_param($rhost['count'], PARAM_INT);
357 $url_is_local = stristr($url , $CFG->wwwroot);
358 if (!empty($name) && !empty($count) && empty($url_is_local)) {
359 $localuser->mnet_foreign_host_array[] = array('name' => $name,
360 'url' => $url,
361 'count' => $count);
366 $localuser->{$key} = $val;
369 $localuser->mnethostid = $remotepeer->id;
371 $bool = update_record('user', addslashes_object($localuser));
372 if (!$bool) {
373 // TODO: Jonathan to clean up mess
374 // Actually, this should never happen (modulo race conditions) - ML
375 error("updating user failed in mnet/auth/confirm_mnet_session ");
378 // set up the session
379 $mnet_session = get_record('mnet_session',
380 'userid', $localuser->id,
381 'mnethostid', $remotepeer->id,
382 'useragent', sha1($_SERVER['HTTP_USER_AGENT']));
383 if ($mnet_session == false) {
384 $mnet_session = new object();
385 $mnet_session->mnethostid = $remotepeer->id;
386 $mnet_session->userid = $localuser->id;
387 $mnet_session->username = $localuser->username;
388 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
389 $mnet_session->token = $token; // Needed to support simultaneous sessions
390 // and preserving DB rec uniqueness
391 $mnet_session->confirm_timeout = time();
392 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
393 $mnet_session->session_id = session_id();
394 if (! $mnet_session->id = insert_record('mnet_session', addslashes_object($mnet_session))) {
395 error(get_string('databaseerror', 'mnet'));
397 } else {
398 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
399 update_record('mnet_session', addslashes_object($mnet_session));
402 if (!$firsttime) {
403 // repeat customer! let the IDP know about enrolments
404 // we have for this user.
405 // set up the RPC request
406 $mnetrequest = new mnet_xmlrpc_client();
407 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
409 // pass username and an assoc array of "my courses"
410 // with info so that the IDP can maintain mnet_enrol_assignments
411 $mnetrequest->add_param($remoteuser->username);
412 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary,
413 startdate, cost, currency, defaultrole, visible';
414 $courses = get_my_courses($localuser->id, 'visible DESC,sortorder ASC', $fields);
415 if (is_array($courses) && !empty($courses)) {
416 // Second request to do the JOINs that we'd have done
417 // inside get_my_courses() if we had been allowed
418 $sql = "SELECT c.id,
419 cc.name AS cat_name, cc.description AS cat_description,
420 r.shortname as defaultrolename
421 FROM {$CFG->prefix}course c
422 JOIN {$CFG->prefix}course_categories cc ON c.category = cc.id
423 LEFT OUTER JOIN {$CFG->prefix}role r ON c.defaultrole = r.id
424 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
425 $extra = get_records_sql($sql);
427 $keys = array_keys($courses);
428 $defaultrolename = get_field('role', 'shortname', 'id', $CFG->defaultcourseroleid);
429 foreach ($keys AS $id) {
430 if ($courses[$id]->visible == 0) {
431 unset($courses[$id]);
432 continue;
434 $courses[$id]->cat_id = $courses[$id]->category;
435 $courses[$id]->defaultroleid = $courses[$id]->defaultrole;
436 unset($courses[$id]->category);
437 unset($courses[$id]->defaultrole);
438 unset($courses[$id]->visible);
440 $courses[$id]->cat_name = $extra[$id]->cat_name;
441 $courses[$id]->cat_description = $extra[$id]->cat_description;
442 if (!empty($extra[$id]->defaultrolename)) {
443 $courses[$id]->defaultrolename = $extra[$id]->defaultrolename;
444 } else {
445 $courses[$id]->defaultrolename = $defaultrolename;
447 // coerce to array
448 $courses[$id] = (array)$courses[$id];
450 } else {
451 // if the array is empty, send it anyway
452 // we may be clearing out stale entries
453 $courses = array();
455 $mnetrequest->add_param($courses);
457 // Call 0800-RPC Now! -- we don't care too much if it fails
458 // as it's just informational.
459 if ($mnetrequest->send($remotepeer) === false) {
460 // error_log(print_r($mnetrequest->error,1));
464 return $localuser;
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
473 * @param string $username The username
474 * @param string $courses Assoc array of courses following the structure of mnet_enrol_course
475 * @return bool
477 function update_enrolments($username, $courses) {
478 global $MNET_REMOTE_CLIENT, $CFG;
480 if (empty($username) || !is_array($courses)) {
481 return false;
483 // make sure it is a user we have an in active session
484 // with that host...
485 $userid = get_field('mnet_session', 'userid',
486 'username', addslashes($username),
487 'mnethostid', (int)$MNET_REMOTE_CLIENT->id);
488 if (!$userid) {
489 return false;
492 if (empty($courses)) { // no courses? clear out quickly
493 delete_records('mnet_enrol_assignments',
494 'hostid', (int)$MNET_REMOTE_CLIENT->id,
495 'userid', $userid);
496 return true;
499 // IMPORTANT: Ask for remoteid as the first element in the query, so
500 // that the array that comes back is indexed on the same field as the
501 // array that we have received from the remote client
502 $sql = '
503 SELECT
504 c.remoteid,
505 c.id,
506 c.cat_id,
507 c.cat_name,
508 c.cat_description,
509 c.sortorder,
510 c.fullname,
511 c.shortname,
512 c.idnumber,
513 c.summary,
514 c.startdate,
515 c.cost,
516 c.currency,
517 c.defaultroleid,
518 c.defaultrolename,
519 a.id as assignmentid
520 FROM
521 '.$CFG->prefix.'mnet_enrol_course c
522 LEFT JOIN
523 '.$CFG->prefix.'mnet_enrol_assignments a
525 (a.courseid = c.id AND
526 a.hostid = c.hostid AND
527 a.userid = \''.$userid.'\')
528 WHERE
529 c.hostid = \''.(int)$MNET_REMOTE_CLIENT->id.'\'';
531 $currentcourses = get_records_sql($sql);
533 $local_courseid_array = array();
534 foreach($courses as $course) {
536 $course['remoteid'] = $course['id'];
537 $course['hostid'] = (int)$MNET_REMOTE_CLIENT->id;
538 $userisregd = false;
540 // First up - do we have a record for this course?
541 if (!array_key_exists($course['remoteid'], $currentcourses)) {
542 // No record - we must create it
543 $course['id'] = insert_record('mnet_enrol_course', addslashes_object((object)$course));
544 $currentcourse = (object)$course;
545 } else {
546 // Pointer to current course:
547 $currentcourse =& $currentcourses[$course['remoteid']];
548 // We have a record - is it up-to-date?
549 $course['id'] = $currentcourse->id;
551 $saveflag = false;
553 foreach($course as $key => $value) {
554 if ($currentcourse->$key != $value) {
555 $saveflag = true;
556 $currentcourse->$key = $value;
560 if ($saveflag) {
561 update_record('mnet_enrol_course', addslashes_object($currentcourse));
564 if (isset($currentcourse->assignmentid) && is_numeric($currentcourse->assignmentid)) {
565 $userisregd = true;
569 // By this point, we should always have a $dataObj->id
570 $local_courseid_array[] = $course['id'];
572 // Do we have a record for this assignment?
573 if ($userisregd) {
574 // Yes - we know about this one already
575 // We don't want to do updates because the new data is probably
576 // 'less complete' than the data we have.
577 } else {
578 // No - create a record
579 $assignObj = new stdClass();
580 $assignObj->userid = $userid;
581 $assignObj->hostid = (int)$MNET_REMOTE_CLIENT->id;
582 $assignObj->courseid = $course['id'];
583 $assignObj->rolename = $course['defaultrolename'];
584 $assignObj->id = insert_record('mnet_enrol_assignments', addslashes_object($assignObj));
588 // Clean up courses that the user is no longer enrolled in.
589 $local_courseid_string = implode(', ', $local_courseid_array);
590 $whereclause = " userid = '$userid' AND hostid = '{$MNET_REMOTE_CLIENT->id}' AND courseid NOT IN ($local_courseid_string)";
591 delete_records_select('mnet_enrol_assignments', $whereclause);
595 * Returns true if this authentication plugin is 'internal'.
597 * @return bool
599 function is_internal() {
600 return false;
604 * Returns true if this authentication plugin can change the user's
605 * password.
607 * @return bool
609 function can_change_password() {
610 //TODO: it should be able to redirect, right?
611 return false;
615 * Returns the URL for changing the user's pw, or false if the default can
616 * be used.
618 * @return string
620 function change_password_url() {
621 return '';
625 * Prints a form for configuring this authentication plugin.
627 * This function is called from admin/auth.php, and outputs a full page with
628 * a form for configuring this plugin.
630 * @param array $page An object containing all the data for this page.
632 function config_form($config, $err, $user_fields) {
633 global $CFG;
635 $query = "
636 SELECT
637 h.id,
638 h.name as hostname,
639 h.wwwroot,
640 h2idp.publish as idppublish,
641 h2idp.subscribe as idpsubscribe,
642 idp.name as idpname,
643 h2sp.publish as sppublish,
644 h2sp.subscribe as spsubscribe,
645 sp.name as spname
646 FROM
647 {$CFG->prefix}mnet_host h
648 LEFT JOIN
649 {$CFG->prefix}mnet_host2service h2idp
651 (h.id = h2idp.hostid AND
652 (h2idp.publish = 1 OR
653 h2idp.subscribe = 1))
654 INNER JOIN
655 {$CFG->prefix}mnet_service idp
657 (h2idp.serviceid = idp.id AND
658 idp.name = 'sso_idp')
659 LEFT JOIN
660 {$CFG->prefix}mnet_host2service h2sp
662 (h.id = h2sp.hostid AND
663 (h2sp.publish = 1 OR
664 h2sp.subscribe = 1))
665 INNER JOIN
666 {$CFG->prefix}mnet_service sp
668 (h2sp.serviceid = sp.id AND
669 sp.name = 'sso_sp')
670 WHERE
671 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
672 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
673 h.id != {$CFG->mnet_localhost_id}
674 ORDER BY
675 h.name ASC";
677 $id_providers = array();
678 $service_providers = array();
679 if ($resultset = get_records_sql($query)) {
680 foreach($resultset as $hostservice) {
681 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
682 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
684 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
685 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
690 include "config.html";
694 * Processes and stores configuration data for this authentication plugin.
696 function process_config($config) {
697 // set to defaults if undefined
698 if (!isset ($config->rpc_negotiation_timeout)) {
699 $config->rpc_negotiation_timeout = '30';
701 if (!isset ($config->auto_add_remote_users)) {
702 $config->auto_add_remote_users = '0';
705 // save settings
706 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth/mnet');
707 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth/mnet');
709 return true;
713 * Poll the IdP server to let it know that a user it has authenticated is still
714 * online
716 * @return void
718 function keepalive_client() {
719 global $CFG, $MNET;
720 $cutoff = time() - 300; // TODO - find out what the remote server's session
721 // cutoff is, and preempt that
723 $sql = "
724 select
726 username,
727 mnethostid
728 from
729 {$CFG->prefix}user
730 where
731 lastaccess > '$cutoff' AND
732 mnethostid != '{$CFG->mnet_localhost_id}'
733 order by
734 mnethostid";
736 $immigrants = get_records_sql($sql);
738 if ($immigrants == false) {
739 return true;
742 $usersArray = array();
743 foreach($immigrants as $immigrant) {
744 $usersArray[$immigrant->mnethostid][] = $immigrant->username;
747 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
748 foreach($usersArray as $mnethostid => $users) {
749 $mnet_peer = new mnet_peer();
750 $mnet_peer->set_id($mnethostid);
752 $mnet_request = new mnet_xmlrpc_client();
753 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
755 // set $token and $useragent parameters
756 $mnet_request->add_param($users);
758 if ($mnet_request->send($mnet_peer) === true) {
759 if (!isset($mnet_request->response['code'])) {
760 debugging("Server side error has occured on host $mnethostid");
761 continue;
762 } elseif ($mnet_request->response['code'] > 0) {
763 debugging($mnet_request->response['message']);
766 if (!isset($mnet_request->response['last log id'])) {
767 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
768 continue;
770 } else {
771 debugging("Server side error has occured on host $mnethostid: " .
772 join("\n", $mnet_request->error));
773 break;
776 $query = "SELECT
777 l.id as remoteid,
778 l.time,
779 l.userid,
780 l.ip,
781 l.course,
782 l.module,
783 l.cmid,
784 l.action,
785 l.url,
786 l.info,
787 c.fullname as coursename,
788 c.modinfo as modinfo,
789 u.username
790 FROM
791 {$CFG->prefix}user u,
792 {$CFG->prefix}log l,
793 {$CFG->prefix}course c
794 WHERE
795 l.userid = u.id AND
796 u.mnethostid = '$mnethostid' AND
797 l.id > '".$mnet_request->response['last log id']."' AND
798 c.id = l.course
799 ORDER BY
800 remoteid ASC";
802 $results = get_records_sql($query);
804 if (false == $results) continue;
806 $param = array();
808 foreach($results as $result) {
809 if (!empty($result->modinfo) && !empty($result->cmid)) {
810 $modinfo = unserialize($result->modinfo);
811 unset($result->modinfo);
812 $modulearray = array();
813 foreach($modinfo as $module) {
814 $modulearray[$module->cm] = urldecode($module->name);
816 $result->resource_name = $modulearray[$result->cmid];
817 } else {
818 $result->resource_name = '';
821 $param[] = array (
822 'remoteid' => $result->remoteid,
823 'time' => $result->time,
824 'userid' => $result->userid,
825 'ip' => $result->ip,
826 'course' => $result->course,
827 'coursename' => $result->coursename,
828 'module' => $result->module,
829 'cmid' => $result->cmid,
830 'action' => $result->action,
831 'url' => $result->url,
832 'info' => $result->info,
833 'resource_name' => $result->resource_name,
834 'username' => $result->username
838 unset($result);
840 $mnet_request = new mnet_xmlrpc_client();
841 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
843 // set $token and $useragent parameters
844 $mnet_request->add_param($param);
846 if ($mnet_request->send($mnet_peer) === true) {
847 if ($mnet_request->response['code'] > 0) {
848 debugging($mnet_request->response['message']);
850 } else {
851 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
857 * Receives an array of log entries from an SP and adds them to the mnet_log
858 * table
860 * @param array $array An array of usernames
861 * @return string "All ok" or an error message
863 function refresh_log($array) {
864 global $CFG, $MNET_REMOTE_CLIENT;
866 // We don't want to output anything to the client machine
867 $start = ob_start();
869 $returnString = '';
870 begin_sql();
871 $useridarray = array();
873 foreach($array as $logEntry) {
874 $logEntryObj = (object)$logEntry;
875 $logEntryObj->hostid = $MNET_REMOTE_CLIENT->id;
877 if (isset($useridarray[$logEntryObj->username])) {
878 $logEntryObj->userid = $useridarray[$logEntryObj->username];
879 } else {
880 $logEntryObj->userid = get_field('user','id','username',$logEntryObj->username);
881 if ($logEntryObj->userid == false) {
882 $logEntryObj->userid = 0;
884 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
887 unset($logEntryObj->username);
889 $insertok = insert_record('mnet_log', addslashes_object($logEntryObj), false);
891 if ($insertok) {
892 $MNET_REMOTE_CLIENT->last_log_id = $logEntryObj->remoteid;
893 } else {
894 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
897 $MNET_REMOTE_CLIENT->commit();
898 commit_sql();
900 $end = ob_end_clean();
902 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
903 return array('code' => 1, 'message' => $returnString);
907 * Receives an array of usernames from a remote machine and prods their
908 * sessions to keep them alive
910 * @param array $array An array of usernames
911 * @return string "All ok" or an error message
913 function keepalive_server($array) {
914 global $MNET_REMOTE_CLIENT, $CFG;
916 $CFG->usesid = true;
917 // Addslashes to all usernames, so we can build the query string real
918 // simply with 'implode'
919 $array = array_map('addslashes', $array);
921 // We don't want to output anything to the client machine
922 $start = ob_start();
924 // We'll get session records in batches of 30
925 $superArray = array_chunk($array, 30);
927 $returnString = '';
929 foreach($superArray as $subArray) {
930 $subArray = array_values($subArray);
931 $instring = "('".implode("', '",$subArray)."')";
932 $query = "select id, session_id, username from {$CFG->prefix}mnet_session where username in $instring";
933 $results = get_records_sql($query);
935 if ($results == false) {
936 // We seem to have a username that breaks our query:
937 // TODO: Handle this error appropriately
938 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
939 } else {
940 // TODO: This process of killing and re-starting the session
941 // will cause PHP to forget any custom session_set_save_handler
942 // stuff. Subsequent attempts to prod existing sessions will
943 // fail, because PHP will look in wherever the default place
944 // may be (files?) and probably create a new session with the
945 // right session ID in that location. If it doesn't have write-
946 // access to that location, then it will fail... not sure how
947 // apparent that will be.
948 // There is no way to capture what the custom session handler
949 // is and then reset it on each pass - I checked that out
950 // already.
951 $sesscache = clone($_SESSION);
952 $sessidcache = session_id();
953 session_write_close();
954 unset($_SESSION);
956 $uc = ini_get('session.use_cookies');
957 ini_set('session.use_cookies', false);
958 foreach($results as $emigrant) {
960 unset($_SESSION);
961 session_name('MoodleSession'.$CFG->sessioncookie);
962 session_id($emigrant->session_id);
963 session_start();
964 session_write_close();
967 ini_set('session.use_cookies', $uc);
968 session_name('MoodleSession'.$CFG->sessioncookie);
969 session_id($sessidcache);
970 session_start();
971 $_SESSION = clone($sesscache);
972 session_write_close();
976 $end = ob_end_clean();
978 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
979 return array('code' => 1, 'message' => $returnString, 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
983 * Cron function will be called automatically by cron.php every 5 minutes
985 * @return void
987 function cron() {
989 // run the keepalive client
990 $this->keepalive_client();
992 // admin/cron.php should have run srand for us
993 $random100 = rand(0,100);
994 if ($random100 < 10) { // Approximately 10% of the time.
995 // nuke olden sessions
996 $longtime = time() - (1 * 3600 * 24);
997 delete_records_select('mnet_session', "expires < $longtime");
1002 * Cleanup any remote mnet_sessions, kill the local mnet_session data
1004 * This is called by require_logout in moodlelib
1006 * @return void
1008 function prelogout_hook() {
1009 global $MNET, $CFG, $USER;
1010 if (!is_enabled_auth('mnet')) {
1011 return;
1014 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1016 // If the user is local to this Moodle:
1017 if ($USER->mnethostid == $MNET->id) {
1018 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1020 // Else the user has hit 'logout' at a Service Provider Moodle:
1021 } else {
1022 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1028 * The SP uses this function to kill the session on the parent IdP
1030 * @param string $username Username for session to kill
1031 * @param string $useragent SHA1 hash of user agent to look for
1032 * @return string A plaintext report of what has happened
1034 function kill_parent($username, $useragent) {
1035 global $CFG, $USER;
1036 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1037 $sql = "
1038 select
1040 from
1041 {$CFG->prefix}mnet_session s
1042 where
1043 s.username = '".addslashes($username)."' AND
1044 s.useragent = '$useragent' AND
1045 s.mnethostid = '{$USER->mnethostid}'";
1047 $mnetsessions = get_records_sql($sql);
1049 $ignore = delete_records('mnet_session',
1050 'username', addslashes($username),
1051 'useragent', $useragent,
1052 'mnethostid', $USER->mnethostid);
1054 if (false != $mnetsessions) {
1055 $mnet_peer = new mnet_peer();
1056 $mnet_peer->set_id($USER->mnethostid);
1058 $mnet_request = new mnet_xmlrpc_client();
1059 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1061 // set $token and $useragent parameters
1062 $mnet_request->add_param($username);
1063 $mnet_request->add_param($useragent);
1064 if ($mnet_request->send($mnet_peer) === false) {
1065 debugging(join("\n", $mnet_request->error));
1066 return false;
1070 $_SESSION = array();
1071 return true;
1075 * The IdP uses this function to kill child sessions on other hosts
1077 * @param string $username Username for session to kill
1078 * @param string $useragent SHA1 hash of user agent to look for
1079 * @return string A plaintext report of what has happened
1081 function kill_children($username, $useragent) {
1082 global $CFG, $USER, $MNET_REMOTE_CLIENT;
1083 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1085 $userid = get_field('user', 'id', 'mnethostid', $CFG->mnet_localhost_id, 'username', addslashes($username));
1087 $returnstring = '';
1088 $sql = "
1089 select
1091 from
1092 {$CFG->prefix}mnet_session s
1093 where
1094 s.userid = '{$userid}' AND
1095 s.useragent = '{$useragent}'";
1097 // If we are being executed from a remote machine (client) we don't have
1098 // to kill the moodle session on that machine.
1099 if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1100 $excludeid = $MNET_REMOTE_CLIENT->id;
1101 } else {
1102 $excludeid = -1;
1105 $mnetsessions = get_records_sql($sql);
1107 if (false == $mnetsessions) {
1108 $returnstring .= "Could find no remote sessions\n$sql\n";
1109 $mnetsessions = array();
1112 foreach($mnetsessions as $mnetsession) {
1113 $returnstring .= "Deleting session\n";
1115 if ($mnetsession->mnethostid == $excludeid) continue;
1117 $mnet_peer = new mnet_peer();
1118 $mnet_peer->set_id($mnetsession->mnethostid);
1120 $mnet_request = new mnet_xmlrpc_client();
1121 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1123 // set $token and $useragent parameters
1124 $mnet_request->add_param($username);
1125 $mnet_request->add_param($useragent);
1126 if ($mnet_request->send($mnet_peer) === false) {
1127 debugging("Server side error has occured on host $mnethostid: " .
1128 join("\n", $mnet_request->error));
1132 $ignore = delete_records('mnet_session',
1133 'useragent', $useragent,
1134 'userid', $userid);
1136 if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1137 $start = ob_start();
1139 $uc = ini_get('session.use_cookies');
1140 ini_set('session.use_cookies', false);
1141 $sesscache = clone($_SESSION);
1142 $sessidcache = session_id();
1143 session_write_close();
1144 unset($_SESSION);
1147 session_id($mnetsession->session_id);
1148 session_start();
1149 session_unregister("USER");
1150 session_unregister("SESSION");
1151 unset($_SESSION);
1152 $_SESSION = array();
1153 session_write_close();
1156 ini_set('session.use_cookies', $uc);
1157 session_name('MoodleSession'.$CFG->sessioncookie);
1158 session_id($sessidcache);
1159 session_start();
1160 $_SESSION = clone($sesscache);
1161 session_write_close();
1163 $end = ob_end_clean();
1164 } else {
1165 $_SESSION = array();
1167 return $returnstring;
1171 * TODO:Untested When the IdP requests that child sessions are terminated,
1172 * this function will be called on each of the child hosts. The machine that
1173 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1175 * @param string $username Username for session to kill
1176 * @param string $useragent SHA1 hash of user agent to look for
1177 * @return bool True on success
1179 function kill_child($username, $useragent) {
1180 global $CFG, $MNET_REMOTE_CLIENT;
1181 $session = get_record('mnet_session', 'username', addslashes($username), 'mnethostid', $MNET_REMOTE_CLIENT->id, 'useragent', $useragent);
1182 if (false != $session) {
1183 $start = ob_start();
1185 $uc = ini_get('session.use_cookies');
1186 ini_set('session.use_cookies', false);
1187 $sesscache = clone($_SESSION);
1188 $sessidcache = session_id();
1189 session_write_close();
1190 unset($_SESSION);
1193 session_id($session->session_id);
1194 session_start();
1195 session_unregister("USER");
1196 session_unregister("SESSION");
1197 unset($_SESSION);
1198 $_SESSION = array();
1199 session_write_close();
1202 ini_set('session.use_cookies', $uc);
1203 session_name('MoodleSession'.$CFG->sessioncookie);
1204 session_id($sessidcache);
1205 session_start();
1206 $_SESSION = clone($sesscache);
1207 session_write_close();
1209 $end = ob_end_clean();
1210 return true;
1212 return false;
1216 * To delete a host, we must delete all current sessions that users from
1217 * that host are currently engaged in.
1219 * @param string $sessionidarray An array of session hashes
1220 * @return bool True on success
1222 function end_local_sessions(&$sessionArray) {
1223 global $CFG;
1224 if (is_array($sessionArray)) {
1225 $start = ob_start();
1227 $uc = ini_get('session.use_cookies');
1228 ini_set('session.use_cookies', false);
1229 $sesscache = clone($_SESSION);
1230 $sessidcache = session_id();
1231 session_write_close();
1232 unset($_SESSION);
1234 while($session = array_pop($sessionArray)) {
1235 session_id($session->session_id);
1236 session_start();
1237 session_unregister("USER");
1238 session_unregister("SESSION");
1239 unset($_SESSION);
1240 $_SESSION = array();
1241 session_write_close();
1244 ini_set('session.use_cookies', $uc);
1245 session_name('MoodleSession'.$CFG->sessioncookie);
1246 session_id($sessidcache);
1247 session_start();
1248 $_SESSION = clone($sesscache);
1250 $end = ob_end_clean();
1251 return true;
1253 return false;
1257 * Returns the user's image as a base64 encoded string.
1259 * @param int $userid The id of the user
1260 * @return string The encoded image
1262 function fetch_user_image($username) {
1263 global $CFG;
1265 if ($user = get_record('user', 'username', addslashes($username), 'mnethostid', $CFG->mnet_localhost_id)) {
1266 $filename1 = "{$CFG->dataroot}/users/{$user->id}/f1.jpg";
1267 $filename2 = "{$CFG->dataroot}/users/{$user->id}/f2.jpg";
1268 $return = array();
1269 if (file_exists($filename1)) {
1270 $return['f1'] = base64_encode(file_get_contents($filename1));
1272 if (file_exists($filename2)) {
1273 $return['f2'] = base64_encode(file_get_contents($filename2));
1275 return $return;
1277 return false;
1281 * Returns the theme information and logo url as strings.
1283 * @return string The theme info
1285 function fetch_theme_info() {
1286 global $CFG;
1288 $themename = "$CFG->theme";
1289 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1291 $return['themename'] = $themename;
1292 $return['logourl'] = $logourl;
1293 return $return;
1297 * Determines if an MNET host is providing the nominated service.
1299 * @param int $mnethostid The id of the remote host
1300 * @param string $servicename The name of the service
1301 * @return bool Whether the service is available on the remote host
1303 function has_service($mnethostid, $servicename) {
1304 global $CFG;
1306 $sql = "
1307 SELECT
1308 svc.id as serviceid,
1309 svc.name,
1310 svc.description,
1311 svc.offer,
1312 svc.apiversion,
1313 h2s.id as h2s_id
1314 FROM
1315 {$CFG->prefix}mnet_service svc,
1316 {$CFG->prefix}mnet_host2service h2s
1317 WHERE
1318 h2s.hostid = '$mnethostid' AND
1319 h2s.serviceid = svc.id AND
1320 svc.name = '$servicename' AND
1321 h2s.subscribe = '1'";
1323 return get_records_sql($sql);
1327 * Checks the MNET access control table to see if the username/mnethost
1328 * is permitted to login to this moodle.
1330 * @param string $username The username
1331 * @param int $mnethostid The id of the remote mnethost
1332 * @return bool Whether the user can login from the remote host
1334 function can_login_remotely($username, $mnethostid) {
1335 $accessctrl = 'allow';
1336 $aclrecord = get_record('mnet_sso_access_control', 'username', addslashes($username), 'mnet_host_id', $mnethostid);
1337 if (!empty($aclrecord)) {
1338 $accessctrl = $aclrecord->accessctrl;
1340 return $accessctrl == 'allow';
1343 function logoutpage_hook() {
1344 global $USER, $CFG, $redirect;
1346 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1347 $host = get_record('mnet_host', 'id', $USER->mnethostid);
1348 $redirect = $host->wwwroot.'/';