MDL-22631 Added some clean_param calls to clean the $_GET data and also added lots...
[moodle.git] / auth / mnet / auth.php
blob42a1f68dd98fa721d5e07a525a838d8ca2022265
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_record('user', '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 = make_user_directory($user->id, true) . "/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))
175 or is_mnet_remote_user($USER)
176 or $USER->username == 'guest'
177 or empty($USER->id)) {
178 print_error('notpermittedtojump', 'mnet');
181 // check for SSO publish permission first
182 if ($this->has_service($mnethostid, 'sso_sp') == false) {
183 print_error('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_recursive($mnet_session))) {
212 print_error('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_recursive($mnet_session))) {
221 print_error('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 * after a successful login, land.php will call complete_user_login
235 * which will in turn regenerate the session id.
236 * this means that what is stored in mnet_session table needs updating.
239 function update_session_id() {
240 global $USER;
241 return set_field('mnet_session', 'session_id', session_id(), 'username', $USER->username, 'mnethostid', $USER->mnethostid, 'useragent', sha1($_SERVER['HTTP_USER_AGENT']));
245 * This function confirms the remote (ID provider) host's mnet session
246 * by communicating the token and UA over the XMLRPC transport layer, and
247 * returns the local user record on success.
249 * @param string $token The random session token.
250 * @param string $remotewwwroot The ID provider wwwroot.
251 * @return array The local user record.
253 function confirm_mnet_session($token, $remotewwwroot) {
254 global $CFG, $MNET, $SESSION;
255 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
257 // verify the remote host is configured locally before attempting RPC call
258 if (! $remotehost = get_record('mnet_host', 'wwwroot', $remotewwwroot, 'deleted', 0)) {
259 print_error('notpermittedtoland', 'mnet');
262 // get the originating (ID provider) host info
263 $remotepeer = new mnet_peer();
264 $remotepeer->set_wwwroot($remotewwwroot);
266 // set up the RPC request
267 $mnetrequest = new mnet_xmlrpc_client();
268 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
270 // set $token and $useragent parameters
271 $mnetrequest->add_param($token);
272 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
274 // Thunderbirds are go! Do RPC call and store response
275 if ($mnetrequest->send($remotepeer) === true) {
276 $remoteuser = (object) $mnetrequest->response;
277 } else {
278 foreach ($mnetrequest->error as $errormessage) {
279 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
280 if($code == 702) {
281 $site = get_site();
282 print_error('mnet_session_prohibited', 'mnet', $remotewwwroot, format_string($site->fullname));
283 exit;
285 $message .= "ERROR $code:<br/>$errormessage<br/>";
287 error("RPC auth/mnet/user_authorise:<br/>$message");
289 unset($mnetrequest);
291 if (empty($remoteuser) or empty($remoteuser->username)) {
292 print_error('unknownerror', 'mnet');
293 exit;
296 $firsttime = false;
298 // get the local record for the remote user
299 $localuser = get_record('user', 'username', addslashes($remoteuser->username), 'mnethostid', $remotehost->id);
301 // add the remote user to the database if necessary, and if allowed
302 // TODO: refactor into a separate function
303 if (empty($localuser) || ! $localuser->id) {
304 if (empty($this->config->auto_add_remote_users)) {
305 print_error('nolocaluser2', 'mnet');
307 $remoteuser->mnethostid = $remotehost->id;
308 $remoteuser->firstaccess = time(); // First time user in this server, grab it here
310 if (!$remoteuser->id = insert_record('user', addslashes_recursive($remoteuser))) {
311 print_error('databaseerror', 'mnet');
313 $firsttime = true;
314 $localuser = $remoteuser;
317 // check sso access control list for permission first
318 if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
319 print_error('sso_mnet_login_refused', 'mnet', '', array($localuser->username, $remotehost->name));
322 $session_gc_maxlifetime = 1440;
324 // update the local user record with remote user data
325 foreach ((array) $remoteuser as $key => $val) {
326 if ($key == 'session.gc_maxlifetime') {
327 $session_gc_maxlifetime = $val;
328 continue;
331 // TODO: fetch image if it has changed
332 if ($key == 'imagehash') {
333 $dirname = make_user_directory($localuser->id, true);
334 $filename = "$dirname/f1.jpg";
336 $localhash = '';
337 if (file_exists($filename)) {
338 $localhash = sha1(file_get_contents($filename));
339 } elseif (!file_exists($dirname)) {
340 mkdir($dirname);
343 if ($localhash != $val) {
344 // fetch image from remote host
345 $fetchrequest = new mnet_xmlrpc_client();
346 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
347 $fetchrequest->add_param($localuser->username);
348 if ($fetchrequest->send($remotepeer) === true) {
349 if (strlen($fetchrequest->response['f1']) > 0) {
350 $imagecontents = base64_decode($fetchrequest->response['f1']);
351 file_put_contents($filename, $imagecontents);
352 $localuser->picture = 1;
354 if (strlen($fetchrequest->response['f2']) > 0) {
355 $imagecontents = base64_decode($fetchrequest->response['f2']);
356 file_put_contents($dirname.'/f2.jpg', $imagecontents);
362 if($key == 'myhosts') {
363 $localuser->mnet_foreign_host_array = array();
364 foreach($val as $rhost) {
365 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
366 $url = clean_param($rhost['url'], PARAM_URL);
367 $count = clean_param($rhost['count'], PARAM_INT);
368 $url_is_local = stristr($url , $CFG->wwwroot);
369 if (!empty($name) && !empty($count) && empty($url_is_local)) {
370 $localuser->mnet_foreign_host_array[] = array('name' => $name,
371 'url' => $url,
372 'count' => $count);
377 $localuser->{$key} = $val;
380 $localuser->mnethostid = $remotepeer->id;
381 if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
382 $localuser->firstaccess = time();
385 $bool = update_record('user', addslashes_recursive($localuser));
386 if (!$bool) {
387 // TODO: Jonathan to clean up mess
388 // Actually, this should never happen (modulo race conditions) - ML
389 error("updating user failed in mnet/auth/confirm_mnet_session ");
392 // set up the session
393 $mnet_session = get_record('mnet_session',
394 'userid', $localuser->id,
395 'mnethostid', $remotepeer->id,
396 'useragent', sha1($_SERVER['HTTP_USER_AGENT']));
397 if ($mnet_session == false) {
398 $mnet_session = new object();
399 $mnet_session->mnethostid = $remotepeer->id;
400 $mnet_session->userid = $localuser->id;
401 $mnet_session->username = $localuser->username;
402 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
403 $mnet_session->token = $token; // Needed to support simultaneous sessions
404 // and preserving DB rec uniqueness
405 $mnet_session->confirm_timeout = time();
406 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
407 $mnet_session->session_id = session_id();
408 if (! $mnet_session->id = insert_record('mnet_session', addslashes_recursive($mnet_session))) {
409 print_error('databaseerror', 'mnet');
411 } else {
412 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
413 update_record('mnet_session', addslashes_recursive($mnet_session));
416 if (!$firsttime) {
417 // repeat customer! let the IDP know about enrolments
418 // we have for this user.
419 // set up the RPC request
420 $mnetrequest = new mnet_xmlrpc_client();
421 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
423 // pass username and an assoc array of "my courses"
424 // with info so that the IDP can maintain mnet_enrol_assignments
425 $mnetrequest->add_param($remoteuser->username);
426 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary,
427 startdate, cost, currency, defaultrole, visible';
428 $courses = get_my_courses($localuser->id, 'visible DESC,sortorder ASC', $fields);
429 if (is_array($courses) && !empty($courses)) {
430 // Second request to do the JOINs that we'd have done
431 // inside get_my_courses() if we had been allowed
432 $sql = "SELECT c.id,
433 cc.name AS cat_name, cc.description AS cat_description,
434 r.shortname as defaultrolename
435 FROM {$CFG->prefix}course c
436 JOIN {$CFG->prefix}course_categories cc ON c.category = cc.id
437 LEFT OUTER JOIN {$CFG->prefix}role r ON c.defaultrole = r.id
438 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
439 $extra = get_records_sql($sql);
441 $keys = array_keys($courses);
442 $defaultrolename = get_field('role', 'shortname', 'id', $CFG->defaultcourseroleid);
443 foreach ($keys AS $id) {
444 if ($courses[$id]->visible == 0) {
445 unset($courses[$id]);
446 continue;
448 $courses[$id]->cat_id = $courses[$id]->category;
449 $courses[$id]->defaultroleid = $courses[$id]->defaultrole;
450 unset($courses[$id]->category);
451 unset($courses[$id]->defaultrole);
452 unset($courses[$id]->visible);
454 $courses[$id]->cat_name = $extra[$id]->cat_name;
455 $courses[$id]->cat_description = $extra[$id]->cat_description;
456 if (!empty($extra[$id]->defaultrolename)) {
457 $courses[$id]->defaultrolename = $extra[$id]->defaultrolename;
458 } else {
459 $courses[$id]->defaultrolename = $defaultrolename;
461 // coerce to array
462 $courses[$id] = (array)$courses[$id];
464 } else {
465 // if the array is empty, send it anyway
466 // we may be clearing out stale entries
467 $courses = array();
469 $mnetrequest->add_param($courses);
471 // Call 0800-RPC Now! -- we don't care too much if it fails
472 // as it's just informational.
473 if ($mnetrequest->send($remotepeer) === false) {
474 // error_log(print_r($mnetrequest->error,1));
478 return $localuser;
482 * Invoke this function _on_ the IDP to update it with enrolment info local to
483 * the SP right after calling user_authorise()
485 * Normally called by the SP after calling
487 * @param string $username The username
488 * @param string $courses Assoc array of courses following the structure of mnet_enrol_course
489 * @return bool
491 function update_enrolments($username, $courses) {
492 global $MNET_REMOTE_CLIENT, $CFG;
494 if (empty($username) || !is_array($courses)) {
495 return false;
497 // make sure it is a user we have an in active session
498 // with that host...
499 $userid = get_field('mnet_session', 'userid',
500 'username', addslashes($username),
501 'mnethostid', (int)$MNET_REMOTE_CLIENT->id);
502 if (!$userid) {
503 return false;
506 if (empty($courses)) { // no courses? clear out quickly
507 delete_records('mnet_enrol_assignments',
508 'hostid', (int)$MNET_REMOTE_CLIENT->id,
509 'userid', $userid);
510 return true;
513 // IMPORTANT: Ask for remoteid as the first element in the query, so
514 // that the array that comes back is indexed on the same field as the
515 // array that we have received from the remote client
516 $sql = '
517 SELECT
518 c.remoteid,
519 c.id,
520 c.cat_id,
521 c.cat_name,
522 c.cat_description,
523 c.sortorder,
524 c.fullname,
525 c.shortname,
526 c.idnumber,
527 c.summary,
528 c.startdate,
529 c.cost,
530 c.currency,
531 c.defaultroleid,
532 c.defaultrolename,
533 a.id as assignmentid
534 FROM
535 '.$CFG->prefix.'mnet_enrol_course c
536 LEFT JOIN
537 '.$CFG->prefix.'mnet_enrol_assignments a
539 (a.courseid = c.id AND
540 a.hostid = c.hostid AND
541 a.userid = \''.$userid.'\')
542 WHERE
543 c.hostid = \''.(int)$MNET_REMOTE_CLIENT->id.'\'';
545 $currentcourses = get_records_sql($sql);
547 $local_courseid_array = array();
548 foreach($courses as $course) {
550 $course['remoteid'] = $course['id'];
551 $course['hostid'] = (int)$MNET_REMOTE_CLIENT->id;
552 $userisregd = false;
554 // First up - do we have a record for this course?
555 if (!array_key_exists($course['remoteid'], $currentcourses)) {
556 // No record - we must create it
557 $course['id'] = insert_record('mnet_enrol_course', addslashes_recursive((object)$course));
558 $currentcourse = (object)$course;
559 } else {
560 // Pointer to current course:
561 $currentcourse =& $currentcourses[$course['remoteid']];
562 // We have a record - is it up-to-date?
563 $course['id'] = $currentcourse->id;
565 $saveflag = false;
567 foreach($course as $key => $value) {
568 if ($currentcourse->$key != $value) {
569 $saveflag = true;
570 $currentcourse->$key = $value;
574 if ($saveflag) {
575 update_record('mnet_enrol_course', addslashes_recursive($currentcourse));
578 if (isset($currentcourse->assignmentid) && is_numeric($currentcourse->assignmentid)) {
579 $userisregd = true;
583 // By this point, we should always have a $dataObj->id
584 $local_courseid_array[] = $course['id'];
586 // Do we have a record for this assignment?
587 if ($userisregd) {
588 // Yes - we know about this one already
589 // We don't want to do updates because the new data is probably
590 // 'less complete' than the data we have.
591 } else {
592 // No - create a record
593 $assignObj = new stdClass();
594 $assignObj->userid = $userid;
595 $assignObj->hostid = (int)$MNET_REMOTE_CLIENT->id;
596 $assignObj->courseid = $course['id'];
597 $assignObj->rolename = $course['defaultrolename'];
598 $assignObj->id = insert_record('mnet_enrol_assignments', addslashes_recursive($assignObj));
602 // Clean up courses that the user is no longer enrolled in.
603 $local_courseid_string = implode(', ', $local_courseid_array);
604 $whereclause = " userid = '$userid' AND hostid = '{$MNET_REMOTE_CLIENT->id}' AND courseid NOT IN ($local_courseid_string)";
605 delete_records_select('mnet_enrol_assignments', $whereclause);
608 function prevent_local_passwords() {
609 return true;
613 * Returns true if this authentication plugin is 'internal'.
615 * @return bool
617 function is_internal() {
618 return false;
622 * Returns true if this authentication plugin can change the user's
623 * password.
625 * @return bool
627 function can_change_password() {
628 //TODO: it should be able to redirect, right?
629 return false;
633 * Returns the URL for changing the user's pw, or false if the default can
634 * be used.
636 * @return string
638 function change_password_url() {
639 return '';
643 * Prints a form for configuring this authentication plugin.
645 * This function is called from admin/auth.php, and outputs a full page with
646 * a form for configuring this plugin.
648 * @param array $page An object containing all the data for this page.
650 function config_form($config, $err, $user_fields) {
651 global $CFG;
653 $query = "
654 SELECT
655 h.id,
656 h.name as hostname,
657 h.wwwroot,
658 h2idp.publish as idppublish,
659 h2idp.subscribe as idpsubscribe,
660 idp.name as idpname,
661 h2sp.publish as sppublish,
662 h2sp.subscribe as spsubscribe,
663 sp.name as spname
664 FROM
665 {$CFG->prefix}mnet_host h
666 LEFT JOIN
667 {$CFG->prefix}mnet_host2service h2idp
669 (h.id = h2idp.hostid AND
670 (h2idp.publish = 1 OR
671 h2idp.subscribe = 1))
672 INNER JOIN
673 {$CFG->prefix}mnet_service idp
675 (h2idp.serviceid = idp.id AND
676 idp.name = 'sso_idp')
677 LEFT JOIN
678 {$CFG->prefix}mnet_host2service h2sp
680 (h.id = h2sp.hostid AND
681 (h2sp.publish = 1 OR
682 h2sp.subscribe = 1))
683 INNER JOIN
684 {$CFG->prefix}mnet_service sp
686 (h2sp.serviceid = sp.id AND
687 sp.name = 'sso_sp')
688 WHERE
689 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
690 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
691 h.id != {$CFG->mnet_localhost_id}
692 ORDER BY
693 h.name ASC";
695 $id_providers = array();
696 $service_providers = array();
697 if ($resultset = get_records_sql($query)) {
698 foreach($resultset as $hostservice) {
699 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
700 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
702 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
703 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
708 include "config.html";
712 * Processes and stores configuration data for this authentication plugin.
714 function process_config($config) {
715 // set to defaults if undefined
716 if (!isset ($config->rpc_negotiation_timeout)) {
717 $config->rpc_negotiation_timeout = '30';
719 if (!isset ($config->auto_add_remote_users)) {
720 $config->auto_add_remote_users = '0';
723 // save settings
724 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth/mnet');
725 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth/mnet');
727 return true;
731 * Poll the IdP server to let it know that a user it has authenticated is still
732 * online
734 * @return void
736 function keepalive_client() {
737 global $CFG, $MNET;
738 $cutoff = time() - 300; // TODO - find out what the remote server's session
739 // cutoff is, and preempt that
741 $sql = "
742 select
744 username,
745 mnethostid
746 from
747 {$CFG->prefix}user
748 where
749 lastaccess > '$cutoff' AND
750 mnethostid != '{$CFG->mnet_localhost_id}'
751 order by
752 mnethostid";
754 $immigrants = get_records_sql($sql);
756 if ($immigrants == false) {
757 return true;
760 $usersArray = array();
761 foreach($immigrants as $immigrant) {
762 $usersArray[$immigrant->mnethostid][] = $immigrant->username;
765 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
766 foreach($usersArray as $mnethostid => $users) {
767 $mnet_peer = new mnet_peer();
768 $mnet_peer->set_id($mnethostid);
770 $mnet_request = new mnet_xmlrpc_client();
771 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
773 // set $token and $useragent parameters
774 $mnet_request->add_param($users);
776 if ($mnet_request->send($mnet_peer) === true) {
777 if (!isset($mnet_request->response['code'])) {
778 debugging("Server side error has occured on host $mnethostid");
779 continue;
780 } elseif ($mnet_request->response['code'] > 0) {
781 debugging($mnet_request->response['message']);
784 if (!isset($mnet_request->response['last log id'])) {
785 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
786 continue;
788 } else {
789 debugging("Server side error has occured on host $mnethostid: " .
790 join("\n", $mnet_request->error));
791 break;
793 $mnethostlogssql = '
794 SELECT
795 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
796 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
797 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
798 c.modinfo
799 FROM
801 SELECT
802 l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
803 l.action, l.url, l.info, u.username
804 FROM
805 ' . $CFG->prefix . 'user u
806 INNER JOIN ' . $CFG->prefix . 'log l on l.userid = u.id
807 WHERE
808 u.mnethostid = ' . $mnethostid . '
809 AND l.id > ' . $mnet_request->response['last log id'] . '
810 ORDER BY remoteid ASC
811 LIMIT 500
812 ) mhostlogs
813 INNER JOIN ' . $CFG->prefix . 'course c on c.id = mhostlogs.course
814 ORDER by mhostlogs.remoteid ASC';
816 $mnethostlogs = get_records_sql($mnethostlogssql);
818 if ($mnethostlogs == false) {
819 continue;
822 $processedlogs = array();
824 foreach($mnethostlogs as $hostlog) {
825 // Extract the name of the relevant module instance from the
826 // course modinfo if possible.
827 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
828 $modinfo = unserialize($hostlog->modinfo);
829 unset($hostlog->modinfo);
830 $modulearray = array();
831 foreach($modinfo as $module) {
832 $modulearray[$module->cm] = urldecode($module->name);
834 $hostlog->resource_name = $modulearray[$hostlog->cmid];
835 } else {
836 $hostlog->resource_name = '';
839 $processedlogs[] = array (
840 'remoteid' => $hostlog->remoteid,
841 'time' => $hostlog->time,
842 'userid' => $hostlog->userid,
843 'ip' => $hostlog->ip,
844 'course' => $hostlog->course,
845 'coursename' => $hostlog->coursename,
846 'module' => $hostlog->module,
847 'cmid' => $hostlog->cmid,
848 'action' => $hostlog->action,
849 'url' => $hostlog->url,
850 'info' => $hostlog->info,
851 'resource_name' => $hostlog->resource_name,
852 'username' => $hostlog->username
856 unset($hostlog);
858 $mnet_request = new mnet_xmlrpc_client();
859 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
861 // set $token and $useragent parameters
862 $mnet_request->add_param($processedlogs);
864 if ($mnet_request->send($mnet_peer) === true) {
865 if ($mnet_request->response['code'] > 0) {
866 debugging($mnet_request->response['message']);
868 } else {
869 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
875 * Receives an array of log entries from an SP and adds them to the mnet_log
876 * table
878 * @param array $array An array of usernames
879 * @return string "All ok" or an error message
881 function refresh_log($array) {
882 global $CFG, $MNET_REMOTE_CLIENT;
884 // We don't want to output anything to the client machine
885 $start = ob_start();
887 $returnString = '';
888 begin_sql();
889 $useridarray = array();
891 foreach($array as $logEntry) {
892 $logEntryObj = (object)$logEntry;
893 $logEntryObj->hostid = $MNET_REMOTE_CLIENT->id;
895 if (isset($useridarray[$logEntryObj->username])) {
896 $logEntryObj->userid = $useridarray[$logEntryObj->username];
897 } else {
898 $logEntryObj->userid = get_field('user','id','username',$logEntryObj->username,'mnethostid',(int)$logEntryObj->hostid);
899 if ($logEntryObj->userid == false) {
900 $logEntryObj->userid = 0;
902 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
905 unset($logEntryObj->username);
907 $logEntryObj = $this->trim_logline($logEntryObj);
908 $insertok = insert_record('mnet_log', addslashes_recursive($logEntryObj), false);
910 if ($insertok) {
911 $MNET_REMOTE_CLIENT->last_log_id = $logEntryObj->remoteid;
912 $MNET_REMOTE_CLIENT->updateparams->last_log_id = $logEntryObj->remoteid;
913 } else {
914 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
917 $MNET_REMOTE_CLIENT->commit();
918 commit_sql();
920 $end = ob_end_clean();
922 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
923 return array('code' => 1, 'message' => $returnString);
927 * Receives an array of usernames from a remote machine and prods their
928 * sessions to keep them alive
930 * @param array $array An array of usernames
931 * @return string "All ok" or an error message
933 function keepalive_server($array) {
934 global $MNET_REMOTE_CLIENT, $CFG;
936 $CFG->usesid = true;
937 // Addslashes to all usernames, so we can build the query string real
938 // simply with 'implode'
939 $array = array_map('addslashes', $array);
941 // We don't want to output anything to the client machine
942 $start = ob_start();
944 // We'll get session records in batches of 30
945 $superArray = array_chunk($array, 30);
947 $returnString = '';
949 foreach($superArray as $subArray) {
950 $subArray = array_values($subArray);
951 $instring = "('".implode("', '",$subArray)."')";
952 $query = "select id, session_id, username from {$CFG->prefix}mnet_session where username in $instring";
953 $results = get_records_sql($query);
955 if ($results == false) {
956 // We seem to have a username that breaks our query:
957 // TODO: Handle this error appropriately
958 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
959 } else {
960 // TODO: This process of killing and re-starting the session
961 // will cause PHP to forget any custom session_set_save_handler
962 // stuff. Subsequent attempts to prod existing sessions will
963 // fail, because PHP will look in wherever the default place
964 // may be (files?) and probably create a new session with the
965 // right session ID in that location. If it doesn't have write-
966 // access to that location, then it will fail... not sure how
967 // apparent that will be.
968 // There is no way to capture what the custom session handler
969 // is and then reset it on each pass - I checked that out
970 // already.
971 $sesscache = $_SESSION;
972 $sessidcache = session_id();
973 session_write_close();
974 unset($_SESSION);
976 $uc = ini_get('session.use_cookies');
977 ini_set('session.use_cookies', false);
978 foreach($results as $emigrant) {
980 unset($_SESSION);
981 session_name('MoodleSession'.$CFG->sessioncookie);
982 session_id($emigrant->session_id);
983 session_start();
984 session_write_close();
987 ini_set('session.use_cookies', $uc);
988 session_name('MoodleSession'.$CFG->sessioncookie);
989 session_id($sessidcache);
990 session_start();
991 $_SESSION = $sesscache;
992 session_write_close();
996 $end = ob_end_clean();
998 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
999 return array('code' => 1, 'message' => $returnString, 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
1003 * Cron function will be called automatically by cron.php every 5 minutes
1005 * @return void
1007 function cron() {
1009 // run the keepalive client
1010 $this->keepalive_client();
1012 // admin/cron.php should have run srand for us
1013 $random100 = rand(0,100);
1014 if ($random100 < 10) { // Approximately 10% of the time.
1015 // nuke olden sessions
1016 $longtime = time() - (1 * 3600 * 24);
1017 delete_records_select('mnet_session', "expires < $longtime");
1022 * Cleanup any remote mnet_sessions, kill the local mnet_session data
1024 * This is called by require_logout in moodlelib
1026 * @return void
1028 function prelogout_hook() {
1029 global $MNET, $CFG, $USER;
1030 if (!is_enabled_auth('mnet')) {
1031 return;
1034 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1036 // If the user is local to this Moodle:
1037 if ($USER->mnethostid == $MNET->id) {
1038 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1040 // Else the user has hit 'logout' at a Service Provider Moodle:
1041 } else {
1042 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1048 * The SP uses this function to kill the session on the parent IdP
1050 * @param string $username Username for session to kill
1051 * @param string $useragent SHA1 hash of user agent to look for
1052 * @return string A plaintext report of what has happened
1054 function kill_parent($username, $useragent) {
1055 global $CFG, $USER;
1056 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1057 $sql = "
1058 select
1060 from
1061 {$CFG->prefix}mnet_session s
1062 where
1063 s.username = '".addslashes($username)."' AND
1064 s.useragent = '$useragent' AND
1065 s.mnethostid = '{$USER->mnethostid}'";
1067 $mnetsessions = get_records_sql($sql);
1069 $ignore = delete_records('mnet_session',
1070 'username', addslashes($username),
1071 'useragent', $useragent,
1072 'mnethostid', $USER->mnethostid);
1074 if (false != $mnetsessions) {
1075 $mnet_peer = new mnet_peer();
1076 $mnet_peer->set_id($USER->mnethostid);
1078 $mnet_request = new mnet_xmlrpc_client();
1079 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1081 // set $token and $useragent parameters
1082 $mnet_request->add_param($username);
1083 $mnet_request->add_param($useragent);
1084 if ($mnet_request->send($mnet_peer) === false) {
1085 debugging(join("\n", $mnet_request->error));
1086 return false;
1090 $_SESSION = array();
1091 return true;
1095 * The IdP uses this function to kill child sessions on other hosts
1097 * @param string $username Username for session to kill
1098 * @param string $useragent SHA1 hash of user agent to look for
1099 * @return string A plaintext report of what has happened
1101 function kill_children($username, $useragent) {
1102 global $CFG, $USER, $MNET_REMOTE_CLIENT;
1103 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1105 $userid = get_field('user', 'id', 'mnethostid', $CFG->mnet_localhost_id, 'username', addslashes($username));
1107 $returnstring = '';
1108 $sql = "
1109 select
1111 from
1112 {$CFG->prefix}mnet_session s
1113 where
1114 s.userid = '{$userid}' AND
1115 s.useragent = '{$useragent}'";
1117 // If we are being executed from a remote machine (client) we don't have
1118 // to kill the moodle session on that machine.
1119 if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1120 $excludeid = $MNET_REMOTE_CLIENT->id;
1121 } else {
1122 $excludeid = -1;
1125 $mnetsessions = get_records_sql($sql);
1127 if (false == $mnetsessions) {
1128 $returnstring .= "Could find no remote sessions\n$sql\n";
1129 $mnetsessions = array();
1132 foreach($mnetsessions as $mnetsession) {
1133 $returnstring .= "Deleting session\n";
1135 if ($mnetsession->mnethostid == $excludeid) continue;
1137 $mnet_peer = new mnet_peer();
1138 $mnet_peer->set_id($mnetsession->mnethostid);
1140 $mnet_request = new mnet_xmlrpc_client();
1141 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1143 // set $token and $useragent parameters
1144 $mnet_request->add_param($username);
1145 $mnet_request->add_param($useragent);
1146 if ($mnet_request->send($mnet_peer) === false) {
1147 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1148 join("\n", $mnet_request->error));
1152 $ignore = delete_records('mnet_session',
1153 'useragent', $useragent,
1154 'userid', $userid);
1156 if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1157 $start = ob_start();
1159 $uc = ini_get('session.use_cookies');
1160 ini_set('session.use_cookies', false);
1161 $sesscache = $_SESSION;
1162 $sessidcache = session_id();
1163 session_write_close();
1164 unset($_SESSION);
1167 session_id($mnetsession->session_id);
1168 session_start();
1169 session_unregister("USER");
1170 session_unregister("SESSION");
1171 unset($_SESSION);
1172 $_SESSION = array();
1173 session_write_close();
1176 ini_set('session.use_cookies', $uc);
1177 session_name('MoodleSession'.$CFG->sessioncookie);
1178 session_id($sessidcache);
1179 session_start();
1180 $_SESSION = $sesscache;
1181 session_write_close();
1183 $end = ob_end_clean();
1184 } else {
1185 $_SESSION = array();
1187 return $returnstring;
1191 * TODO:Untested When the IdP requests that child sessions are terminated,
1192 * this function will be called on each of the child hosts. The machine that
1193 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1195 * @param string $username Username for session to kill
1196 * @param string $useragent SHA1 hash of user agent to look for
1197 * @return bool True on success
1199 function kill_child($username, $useragent) {
1200 global $CFG, $MNET_REMOTE_CLIENT;
1201 $session = get_record('mnet_session', 'username', addslashes($username), 'mnethostid', $MNET_REMOTE_CLIENT->id, 'useragent', $useragent);
1202 if (false != $session) {
1203 $start = ob_start();
1205 $uc = ini_get('session.use_cookies');
1206 ini_set('session.use_cookies', false);
1207 $sesscache = $_SESSION;
1208 $sessidcache = session_id();
1209 session_write_close();
1210 unset($_SESSION);
1213 session_id($session->session_id);
1214 session_start();
1215 session_unregister("USER");
1216 session_unregister("SESSION");
1217 unset($_SESSION);
1218 $_SESSION = array();
1219 session_write_close();
1222 ini_set('session.use_cookies', $uc);
1223 session_name('MoodleSession'.$CFG->sessioncookie);
1224 session_id($sessidcache);
1225 session_start();
1226 $_SESSION = $sesscache;
1227 session_write_close();
1229 $end = ob_end_clean();
1230 return true;
1232 return false;
1236 * To delete a host, we must delete all current sessions that users from
1237 * that host are currently engaged in.
1239 * @param string $sessionidarray An array of session hashes
1240 * @return bool True on success
1242 function end_local_sessions(&$sessionArray) {
1243 global $CFG;
1244 if (is_array($sessionArray)) {
1245 $start = ob_start();
1247 $uc = ini_get('session.use_cookies');
1248 ini_set('session.use_cookies', false);
1249 $sesscache = $_SESSION;
1250 $sessidcache = session_id();
1251 session_write_close();
1252 unset($_SESSION);
1254 while($session = array_pop($sessionArray)) {
1255 session_id($session->session_id);
1256 session_start();
1257 session_unregister("USER");
1258 session_unregister("SESSION");
1259 unset($_SESSION);
1260 $_SESSION = array();
1261 session_write_close();
1264 ini_set('session.use_cookies', $uc);
1265 session_name('MoodleSession'.$CFG->sessioncookie);
1266 session_id($sessidcache);
1267 session_start();
1268 $_SESSION = $sesscache;
1270 $end = ob_end_clean();
1271 return true;
1273 return false;
1277 * Returns the user's image as a base64 encoded string.
1279 * @param int $userid The id of the user
1280 * @return string The encoded image
1282 function fetch_user_image($username) {
1283 global $CFG;
1285 if ($user = get_record('user', 'username', addslashes($username), 'mnethostid', $CFG->mnet_localhost_id)) {
1286 $filename1 = make_user_directory($user->id, true) . "/f1.jpg";
1287 $filename2 = make_user_directory($user->id, true) . "/f2.jpg";
1288 $return = array();
1289 if (file_exists($filename1)) {
1290 $return['f1'] = base64_encode(file_get_contents($filename1));
1292 if (file_exists($filename2)) {
1293 $return['f2'] = base64_encode(file_get_contents($filename2));
1295 return $return;
1297 return false;
1301 * Returns the theme information and logo url as strings.
1303 * @return string The theme info
1305 function fetch_theme_info() {
1306 global $CFG;
1308 $themename = "$CFG->theme";
1309 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1311 $return['themename'] = $themename;
1312 $return['logourl'] = $logourl;
1313 return $return;
1317 * Determines if an MNET host is providing the nominated service.
1319 * @param int $mnethostid The id of the remote host
1320 * @param string $servicename The name of the service
1321 * @return bool Whether the service is available on the remote host
1323 function has_service($mnethostid, $servicename) {
1324 global $CFG;
1326 $sql = "
1327 SELECT
1328 svc.id as serviceid,
1329 svc.name,
1330 svc.description,
1331 svc.offer,
1332 svc.apiversion,
1333 h2s.id as h2s_id
1334 FROM
1335 {$CFG->prefix}mnet_host h,
1336 {$CFG->prefix}mnet_service svc,
1337 {$CFG->prefix}mnet_host2service h2s
1338 WHERE
1339 h.deleted = '0' AND
1340 h.id = h2s.hostid AND
1341 h2s.hostid = '$mnethostid' AND
1342 h2s.serviceid = svc.id AND
1343 svc.name = '$servicename' AND
1344 h2s.subscribe = '1'";
1346 return get_records_sql($sql);
1350 * Checks the MNET access control table to see if the username/mnethost
1351 * is permitted to login to this moodle.
1353 * @param string $username The username
1354 * @param int $mnethostid The id of the remote mnethost
1355 * @return bool Whether the user can login from the remote host
1357 function can_login_remotely($username, $mnethostid) {
1358 $accessctrl = 'allow';
1359 $aclrecord = get_record('mnet_sso_access_control', 'username', addslashes($username), 'mnet_host_id', $mnethostid);
1360 if (!empty($aclrecord)) {
1361 $accessctrl = $aclrecord->accessctrl;
1363 return $accessctrl == 'allow';
1366 function logoutpage_hook() {
1367 global $USER, $CFG, $redirect;
1369 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1370 $host = get_record('mnet_host', 'id', $USER->mnethostid);
1371 $redirect = $host->wwwroot.'/';
1376 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1378 * @param object $logline The log information to be trimmed
1379 * @return object The passed logline object trimmed to not exceed storable limits
1381 function trim_logline ($logline) {
1382 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1383 'url' => 255);
1384 foreach ($limits as $property => $limit) {
1385 if (isset($logline->$property)) {
1386 $logline->$property = substr($logline->$property, 0, $limit);
1390 return $logline;