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');
22 * Moodle Network authentication plugin.
24 class auth_plugin_mnet extends auth_plugin_base {
29 function auth_plugin_mnet() {
30 $this->authtype = 'mnet';
31 $this->config = get_config('auth_mnet');
32 $this->mnet = get_mnet_environment();
36 * This function is normally used to determine if the username and password
37 * are correct for local logins. Always returns false, as local users do not
38 * need to login over mnet xmlrpc.
40 * @param string $username The username
41 * @param string $password The password
42 * @return bool Authentication success or failure.
44 function user_login($username, $password) {
45 return false; // print_error("mnetlocal");
49 * Return user data for the provided token, compare with user_agent string.
51 * @param string $token The unique ID provided by remotehost.
52 * @param string $UA User Agent string.
53 * @return array $userdata Array of user info for remote host
55 function user_authorise($token, $useragent) {
56 global $CFG, $SITE, $DB;
57 $remoteclient = get_mnet_remote_client();
58 require_once $CFG->dirroot . '/mnet/xmlrpc/serverlib.php';
60 $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent));
61 if (empty($mnet_session)) {
62 throw new mnet_server_exception(1, 'authfail_nosessionexists');
65 // check session confirm timeout
66 if ($mnet_session->confirm_timeout < time()) {
67 throw new mnet_server_exception(2, 'authfail_sessiontimedout');
70 // session okay, try getting the user
71 if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid))) {
72 throw new mnet_server_exception(3, 'authfail_usermismatch');
75 $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient));
78 $userdata['auth'] = 'mnet';
79 $userdata['wwwroot'] = $this->mnet->wwwroot;
80 $userdata['session.gc_maxlifetime'] = ini_get('session.gc_maxlifetime');
82 if (array_key_exists('picture', $userdata) && !empty($user->picture)) {
83 //TODO: rewrite to use new file storage
85 $imagefile = make_user_directory($user->id, true) . "/f1.jpg";
86 if (file_exists($imagefile)) {
87 $userdata['imagehash'] = sha1(file_get_contents($imagefile));
92 $userdata['myhosts'] = array();
93 if ($courses = enrol_get_users_courses($user->id, false)) {
94 $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
97 $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
99 FROM {mnetservice_enrol_courses} c
100 JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
101 JOIN {mnet_host} h ON h.id = c.hostid
102 WHERE e.userid = ? AND c.hostid = ?
103 GROUP BY h.name, h.wwwroot, h.id";
105 if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) {
106 foreach($courses as $course) {
107 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
115 * Generate a random string for use as an RPC session token.
117 function generate_token() {
118 return sha1(str_shuffle('' . mt_rand() . time()));
122 * Starts an RPC jump session and returns the jump redirect URL.
124 * @param int $mnethostid id of the mnet host to jump to
125 * @param string $wantsurl url to redirect to after the jump (usually on remote system)
126 * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
127 * rather than somewhere inside *its* wwwroot
129 function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
130 global $CFG, $USER, $DB;
131 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
133 // check remote login permissions
134 if (! has_capability('moodle/site:mnetlogintoremote', get_system_context())
135 or is_mnet_remote_user($USER)
138 print_error('notpermittedtojump', 'mnet');
141 // check for SSO publish permission first
142 if ($this->has_service($mnethostid, 'sso_sp') == false) {
143 print_error('hostnotconfiguredforsso', 'mnet');
146 // set RPC timeout to 30 seconds if not configured
147 if (empty($this->config->rpc_negotiation_timeout)) {
148 $this->config->rpc_negotiation_timeout = 30;
149 set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
153 $mnet_peer = new mnet_peer();
154 $mnet_peer->set_id($mnethostid);
156 // set up the session
157 $mnet_session = $DB->get_record('mnet_session',
158 array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
159 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
160 if ($mnet_session == false) {
161 $mnet_session = new stdClass();
162 $mnet_session->mnethostid = $mnethostid;
163 $mnet_session->userid = $USER->id;
164 $mnet_session->username = $USER->username;
165 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
166 $mnet_session->token = $this->generate_token();
167 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
168 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
169 $mnet_session->session_id = session_id();
170 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
172 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
173 $mnet_session->token = $this->generate_token();
174 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
175 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
176 $mnet_session->session_id = session_id();
177 $DB->update_record('mnet_session', $mnet_session);
180 // construct the redirection URL
181 //$transport = mnet_get_protocol($mnet_peer->transport);
182 $wantsurl = urlencode($wantsurl);
183 $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
184 if ($wantsurlbackhere) {
185 $url .= '&remoteurl=1';
192 * This function confirms the remote (ID provider) host's mnet session
193 * by communicating the token and UA over the XMLRPC transport layer, and
194 * returns the local user record on success.
196 * @param string $token The random session token.
197 * @param mnet_peer $remotepeer The ID provider mnet_peer object.
198 * @return array The local user record.
200 function confirm_mnet_session($token, $remotepeer) {
202 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
204 // verify the remote host is configured locally before attempting RPC call
205 if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
206 print_error('notpermittedtoland', 'mnet');
209 // set up the RPC request
210 $mnetrequest = new mnet_xmlrpc_client();
211 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
213 // set $token and $useragent parameters
214 $mnetrequest->add_param($token);
215 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
217 // Thunderbirds are go! Do RPC call and store response
218 if ($mnetrequest->send($remotepeer) === true) {
219 $remoteuser = (object) $mnetrequest->response;
221 foreach ($mnetrequest->error as $errormessage) {
222 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
225 print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname));
228 $message .= "ERROR $code:<br/>$errormessage<br/>";
230 print_error("rpcerror", '', '', $message);
234 if (empty($remoteuser) or empty($remoteuser->username)) {
235 print_error('unknownerror', 'mnet');
239 if (user_not_fully_set_up($remoteuser)) {
240 print_error('notenoughidpinfo', 'mnet');
244 $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
246 $remoteuser->auth = 'mnet';
247 $remoteuser->wwwroot = $remotepeer->wwwroot;
249 // the user may roam from Moodle 1.x where lang has _utf8 suffix
250 // also, make sure that the lang is actually installed, otherwise set site default
251 if (isset($remoteuser->lang)) {
252 $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
254 if (empty($remoteuser->lang)) {
255 if (!empty($CFG->lang)) {
256 $remoteuser->lang = $CFG->lang;
258 $remoteuser->lang = 'en';
263 // get the local record for the remote user
264 $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
266 // add the remote user to the database if necessary, and if allowed
267 // TODO: refactor into a separate function
268 if (empty($localuser) || ! $localuser->id) {
270 if (empty($this->config->auto_add_remote_users)) {
271 print_error('nolocaluser', 'mnet');
272 } See MDL-21327 for why this is commented out
274 $remoteuser->mnethostid = $remotehost->id;
275 $remoteuser->firstaccess = time(); // First time user in this server, grab it here
277 $remoteuser->id = $DB->insert_record('user', $remoteuser);
279 $localuser = $remoteuser;
282 // check sso access control list for permission first
283 if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
284 print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
287 // update the local user record with remote user data
288 foreach ((array) $remoteuser as $key => $val) {
290 // TODO: fetch image if it has changed
291 //TODO: rewrite to use new file storage
292 if ($key == 'imagehash') {
294 $dirname = make_user_directory($localuser->id, true);
295 $filename = "$dirname/f1.jpg";
298 if (file_exists($filename)) {
299 $localhash = sha1(file_get_contents($filename));
300 } elseif (!file_exists($dirname)) {
304 if ($localhash != $val) {
305 // fetch image from remote host
306 $fetchrequest = new mnet_xmlrpc_client();
307 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
308 $fetchrequest->add_param($localuser->username);
309 if ($fetchrequest->send($remotepeer) === true) {
310 if (strlen($fetchrequest->response['f1']) > 0) {
311 $imagecontents = base64_decode($fetchrequest->response['f1']);
312 file_put_contents($filename, $imagecontents);
313 $localuser->picture = 1;
315 if (strlen($fetchrequest->response['f2']) > 0) {
316 $imagecontents = base64_decode($fetchrequest->response['f2']);
317 file_put_contents($dirname.'/f2.jpg', $imagecontents);
324 if($key == 'myhosts') {
325 $localuser->mnet_foreign_host_array = array();
326 foreach($val as $rhost) {
327 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
328 $url = clean_param($rhost['url'], PARAM_URL);
329 $count = clean_param($rhost['count'], PARAM_INT);
330 $url_is_local = stristr($url , $CFG->wwwroot);
331 if (!empty($name) && !empty($count) && empty($url_is_local)) {
332 $localuser->mnet_foreign_host_array[] = array('name' => $name,
339 $localuser->{$key} = $val;
342 $localuser->mnethostid = $remotepeer->id;
343 if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
344 $localuser->firstaccess = time();
347 $DB->update_record('user', $localuser);
350 // repeat customer! let the IDP know about enrolments
351 // we have for this user.
352 // set up the RPC request
353 $mnetrequest = new mnet_xmlrpc_client();
354 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
356 // pass username and an assoc array of "my courses"
357 // with info so that the IDP can maintain mnetservice_enrol_enrolments
358 $mnetrequest->add_param($remoteuser->username);
359 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
360 $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
361 if (is_array($courses) && !empty($courses)) {
362 // Second request to do the JOINs that we'd have done
363 // inside enrol_get_users_courses() if we had been allowed
365 cc.name AS cat_name, cc.description AS cat_description
367 JOIN {course_categories} cc ON c.category = cc.id
368 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
369 $extra = $DB->get_records_sql($sql);
371 $keys = array_keys($courses);
372 $defaultrole = reset(get_archetype_roles('student'));
373 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
374 foreach ($keys AS $id) {
375 if ($courses[$id]->visible == 0) {
376 unset($courses[$id]);
379 $courses[$id]->cat_id = $courses[$id]->category;
380 $courses[$id]->defaultroleid = $defaultrole->id;
381 unset($courses[$id]->category);
382 unset($courses[$id]->visible);
384 $courses[$id]->cat_name = $extra[$id]->cat_name;
385 $courses[$id]->cat_description = $extra[$id]->cat_description;
386 $courses[$id]->defaultrolename = $defaultrole->name;
388 $courses[$id] = (array)$courses[$id];
391 // if the array is empty, send it anyway
392 // we may be clearing out stale entries
395 $mnetrequest->add_param($courses);
397 // Call 0800-RPC Now! -- we don't care too much if it fails
398 // as it's just informational.
399 if ($mnetrequest->send($remotepeer) === false) {
400 // error_log(print_r($mnetrequest->error,1));
409 * creates (or updates) the mnet session once
410 * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
412 * @param stdclass $user the local user (must exist already
413 * @param string $token the jump/land token
414 * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
416 public function update_mnet_session($user, $token, $remotepeer) {
418 $session_gc_maxlifetime = 1440;
419 if (isset($user->session_gc_maxlifetime)) {
420 $session_gc_maxlifetime = $user->session_gc_maxlifetime;
422 if (!$mnet_session = $DB->get_record('mnet_session',
423 array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
424 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
425 $mnet_session = new stdClass();
426 $mnet_session->mnethostid = $remotepeer->id;
427 $mnet_session->userid = $user->id;
428 $mnet_session->username = $user->username;
429 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
430 $mnet_session->token = $token; // Needed to support simultaneous sessions
431 // and preserving DB rec uniqueness
432 $mnet_session->confirm_timeout = time();
433 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
434 $mnet_session->session_id = session_id();
435 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
437 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
438 $DB->update_record('mnet_session', $mnet_session);
445 * Invoke this function _on_ the IDP to update it with enrolment info local to
446 * the SP right after calling user_authorise()
448 * Normally called by the SP after calling user_authorise()
450 * @param string $username The username
451 * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses
454 function update_enrolments($username, $courses) {
456 $remoteclient = get_mnet_remote_client();
458 if (empty($username) || !is_array($courses)) {
461 // make sure it is a user we have an in active session
463 if (!$userid = $DB->get_field('mnet_session', 'userid',
464 array('username'=>$username, 'mnethostid'=>$remoteclient->id))) {
465 throw new mnet_server_exception(1, 'authfail_nosessionexists');
468 if (empty($courses)) { // no courses? clear out quickly
469 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
473 // IMPORTANT: Ask for remoteid as the first element in the query, so
474 // that the array that comes back is indexed on the same field as the
475 // array that we have received from the remote client
476 $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
477 c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
479 FROM {mnetservice_enrol_courses} c
480 LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
481 WHERE e.userid = ? AND c.hostid = ?";
483 $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
485 $local_courseid_array = array();
486 foreach($courses as $ix => $course) {
488 $course['remoteid'] = $course['id'];
489 $course['hostid'] = (int)$remoteclient->id;
492 // if we do not have the the information about the remote course, it is not available
493 // to us for remote enrolment - skip
494 if (array_key_exists($course['remoteid'], $currentcourses)) {
495 // Pointer to current course:
496 $currentcourse =& $currentcourses[$course['remoteid']];
497 // We have a record - is it up-to-date?
498 $course['id'] = $currentcourse->id;
502 foreach($course as $key => $value) {
503 if ($currentcourse->$key != $value) {
505 $currentcourse->$key = $value;
510 $DB->update_record('mnetervice_enrol_courses', $currentcourse);
513 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
517 unset ($courses[$ix]);
521 // By this point, we should always have a $dataObj->id
522 $local_courseid_array[] = $course['id'];
524 // Do we have a record for this assignment?
526 // Yes - we know about this one already
527 // We don't want to do updates because the new data is probably
528 // 'less complete' than the data we have.
530 // No - create a record
531 $assignObj = new stdClass();
532 $assignObj->userid = $userid;
533 $assignObj->hostid = (int)$remoteclient->id;
534 $assignObj->remotecourseid = $course['remoteid'];
535 $assignObj->rolename = $course['defaultrolename'];
536 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
540 // Clean up courses that the user is no longer enrolled in.
541 $local_courseid_string = implode(', ', $local_courseid_array);
542 $whereclause = " userid = ? AND hostid = ? AND courseid NOT IN ($local_courseid_string)";
543 $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
546 function prevent_local_passwords() {
551 * Returns true if this authentication plugin is 'internal'.
555 function is_internal() {
560 * Returns true if this authentication plugin can change the user's
565 function can_change_password() {
566 //TODO: it should be able to redirect, right?
571 * Returns the URL for changing the user's pw, or false if the default can
576 function change_password_url() {
581 * Prints a form for configuring this authentication plugin.
583 * This function is called from admin/auth.php, and outputs a full page with
584 * a form for configuring this plugin.
586 * @param object $config
588 * @param array $user_fields
590 function config_form($config, $err, $user_fields) {
598 h2idp.publish as idppublish,
599 h2idp.subscribe as idpsubscribe,
601 h2sp.publish as sppublish,
602 h2sp.subscribe as spsubscribe,
607 {mnet_host2service} h2idp
609 (h.id = h2idp.hostid AND
610 (h2idp.publish = 1 OR
611 h2idp.subscribe = 1))
615 (h2idp.serviceid = idp.id AND
616 idp.name = 'sso_idp')
618 {mnet_host2service} h2sp
620 (h.id = h2sp.hostid AND
626 (h2sp.serviceid = sp.id AND
629 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
630 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
635 $id_providers = array();
636 $service_providers = array();
637 if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
638 foreach($resultset as $hostservice) {
639 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
640 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
642 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
643 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
648 include "config.html";
652 * Processes and stores configuration data for this authentication plugin.
654 function process_config($config) {
655 // set to defaults if undefined
656 if (!isset ($config->rpc_negotiation_timeout)) {
657 $config->rpc_negotiation_timeout = '30';
660 if (!isset ($config->auto_add_remote_users)) {
661 $config->auto_add_remote_users = '0';
662 } See MDL-21327 for why this is commented out
663 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth_mnet');
667 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
673 * Poll the IdP server to let it know that a user it has authenticated is still
678 function keepalive_client() {
680 $cutoff = time() - 300; // TODO - find out what the remote server's session
681 // cutoff is, and preempt that
696 $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
698 if ($immigrants == false) {
702 $usersArray = array();
703 foreach($immigrants as $immigrant) {
704 $usersArray[$immigrant->mnethostid][] = $immigrant->username;
707 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
708 foreach($usersArray as $mnethostid => $users) {
709 $mnet_peer = new mnet_peer();
710 $mnet_peer->set_id($mnethostid);
712 $mnet_request = new mnet_xmlrpc_client();
713 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
715 // set $token and $useragent parameters
716 $mnet_request->add_param($users);
718 if ($mnet_request->send($mnet_peer) === true) {
719 if (!isset($mnet_request->response['code'])) {
720 debugging("Server side error has occured on host $mnethostid");
722 } elseif ($mnet_request->response['code'] > 0) {
723 debugging($mnet_request->response['message']);
726 if (!isset($mnet_request->response['last log id'])) {
727 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
731 debugging("Server side error has occured on host $mnethostid: " .
732 join("\n", $mnet_request->error));
737 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
738 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
739 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
744 l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
745 l.action, l.url, l.info, u.username
748 INNER JOIN {log} l on l.userid = u.id
752 ORDER BY remoteid ASC
755 INNER JOIN {course} c on c.id = mhostlogs.course
756 ORDER by mhostlogs.remoteid ASC";
758 $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
760 if ($mnethostlogs == false) {
764 $processedlogs = array();
766 foreach($mnethostlogs as $hostlog) {
767 // Extract the name of the relevant module instance from the
768 // course modinfo if possible.
769 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
770 $modinfo = unserialize($hostlog->modinfo);
771 unset($hostlog->modinfo);
772 $modulearray = array();
773 foreach($modinfo as $module) {
774 $modulearray[$module->cm] = $module->name;
776 $hostlog->resource_name = $modulearray[$hostlog->cmid];
778 $hostlog->resource_name = '';
781 $processedlogs[] = array (
782 'remoteid' => $hostlog->remoteid,
783 'time' => $hostlog->time,
784 'userid' => $hostlog->userid,
785 'ip' => $hostlog->ip,
786 'course' => $hostlog->course,
787 'coursename' => $hostlog->coursename,
788 'module' => $hostlog->module,
789 'cmid' => $hostlog->cmid,
790 'action' => $hostlog->action,
791 'url' => $hostlog->url,
792 'info' => $hostlog->info,
793 'resource_name' => $hostlog->resource_name,
794 'username' => $hostlog->username
800 $mnet_request = new mnet_xmlrpc_client();
801 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
803 // set $token and $useragent parameters
804 $mnet_request->add_param($processedlogs);
806 if ($mnet_request->send($mnet_peer) === true) {
807 if ($mnet_request->response['code'] > 0) {
808 debugging($mnet_request->response['message']);
811 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
817 * Receives an array of log entries from an SP and adds them to the mnet_log
820 * @param array $array An array of usernames
821 * @return string "All ok" or an error message
823 function refresh_log($array) {
825 $remoteclient = get_mnet_remote_client();
827 // We don't want to output anything to the client machine
831 $transaction = $DB->start_delegated_transaction();
832 $useridarray = array();
834 foreach($array as $logEntry) {
835 $logEntryObj = (object)$logEntry;
836 $logEntryObj->hostid = $remoteclient->id;
838 if (isset($useridarray[$logEntryObj->username])) {
839 $logEntryObj->userid = $useridarray[$logEntryObj->username];
841 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
842 if ($logEntryObj->userid == false) {
843 $logEntryObj->userid = 0;
845 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
848 unset($logEntryObj->username);
850 $logEntryObj = $this->trim_logline($logEntryObj);
851 $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
854 $remoteclient->last_log_id = $logEntryObj->remoteid;
856 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
859 $remoteclient->commit();
860 $transaction->allow_commit();
862 $end = ob_end_clean();
864 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
865 return array('code' => 1, 'message' => $returnString);
869 * Receives an array of usernames from a remote machine and prods their
870 * sessions to keep them alive
872 * @param array $array An array of usernames
873 * @return string "All ok" or an error message
875 function keepalive_server($array) {
877 $remoteclient = get_mnet_remote_client();
881 // We don't want to output anything to the client machine
884 // We'll get session records in batches of 30
885 $superArray = array_chunk($array, 30);
889 foreach($superArray as $subArray) {
890 $subArray = array_values($subArray);
891 $instring = "('".implode("', '",$subArray)."')";
892 $query = "select id, session_id, username from {mnet_session} where username in $instring";
893 $results = $DB->get_records_sql($query);
895 if ($results == false) {
896 // We seem to have a username that breaks our query:
897 // TODO: Handle this error appropriately
898 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
900 foreach($results as $emigrant) {
901 session_touch($emigrant->session_id);
906 $end = ob_end_clean();
908 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
909 return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
913 * Cron function will be called automatically by cron.php every 5 minutes
920 // run the keepalive client
921 $this->keepalive_client();
923 // admin/cron.php should have run srand for us
924 $random100 = rand(0,100);
925 if ($random100 < 10) { // Approximately 10% of the time.
926 // nuke olden sessions
927 $longtime = time() - (1 * 3600 * 24);
928 $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
933 * Cleanup any remote mnet_sessions, kill the local mnet_session data
935 * This is called by require_logout in moodlelib
939 function prelogout_hook() {
942 if (!is_enabled_auth('mnet')) {
946 // If the user is local to this Moodle:
947 if ($USER->mnethostid == $this->mnet->id) {
948 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
950 // Else the user has hit 'logout' at a Service Provider Moodle:
952 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
958 * The SP uses this function to kill the session on the parent IdP
960 * @param string $username Username for session to kill
961 * @param string $useragent SHA1 hash of user agent to look for
962 * @return string A plaintext report of what has happened
964 function kill_parent($username, $useragent) {
965 global $CFG, $USER, $DB;
967 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
978 $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
980 $ignore = $DB->delete_records('mnet_session',
981 array('username'=>$username,
982 'useragent'=>$useragent,
983 'mnethostid'=>$USER->mnethostid));
985 if (false != $mnetsessions) {
986 $mnet_peer = new mnet_peer();
987 $mnet_peer->set_id($USER->mnethostid);
989 $mnet_request = new mnet_xmlrpc_client();
990 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
992 // set $token and $useragent parameters
993 $mnet_request->add_param($username);
994 $mnet_request->add_param($useragent);
995 if ($mnet_request->send($mnet_peer) === false) {
996 debugging(join("\n", $mnet_request->error));
1005 * The IdP uses this function to kill child sessions on other hosts
1007 * @param string $username Username for session to kill
1008 * @param string $useragent SHA1 hash of user agent to look for
1009 * @return string A plaintext report of what has happened
1011 function kill_children($username, $useragent) {
1012 global $CFG, $USER, $DB;
1013 $remoteclient = null;
1014 if (defined('MNET_SERVER')) {
1015 $remoteclient = get_mnet_remote_client();
1017 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1019 $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1023 $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1025 if (false == $mnetsessions) {
1026 $returnstring .= "Could find no remote sessions\n";
1027 $mnetsessions = array();
1030 foreach($mnetsessions as $mnetsession) {
1031 // If this script is being executed by a remote peer, that means the user has clicked
1032 // logout on that peer, and the session on that peer can be deleted natively.
1034 if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1037 $returnstring .= "Deleting session\n";
1039 $mnet_peer = new mnet_peer();
1040 $mnet_peer->set_id($mnetsession->mnethostid);
1042 $mnet_request = new mnet_xmlrpc_client();
1043 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1045 // set $token and $useragent parameters
1046 $mnet_request->add_param($username);
1047 $mnet_request->add_param($useragent);
1048 if ($mnet_request->send($mnet_peer) === false) {
1049 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1050 join("\n", $mnet_request->error));
1054 $ignore = $DB->delete_records('mnet_session',
1055 array('useragent'=>$useragent, 'userid'=>$userid));
1057 if (isset($remoteclient) && isset($remoteclient->id)) {
1058 session_kill_user($userid);
1060 return $returnstring;
1064 * When the IdP requests that child sessions are terminated,
1065 * this function will be called on each of the child hosts. The machine that
1066 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1068 * @param string $username Username for session to kill
1069 * @param string $useragent SHA1 hash of user agent to look for
1070 * @return bool True on success
1072 function kill_child($username, $useragent) {
1074 $remoteclient = get_mnet_remote_client();
1075 $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1076 $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1077 if (false != $session) {
1078 session_kill($session->session_id);
1085 * To delete a host, we must delete all current sessions that users from
1086 * that host are currently engaged in.
1088 * @param string $sessionidarray An array of session hashes
1089 * @return bool True on success
1091 function end_local_sessions(&$sessionArray) {
1093 if (is_array($sessionArray)) {
1094 while($session = array_pop($sessionArray)) {
1095 session_kill($session->session_id);
1103 * Returns the user's image as a base64 encoded string.
1105 * @param int $userid The id of the user
1106 * @return string The encoded image
1108 function fetch_user_image($username) {
1111 //TODO: rewrite to use new file storage
1114 if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
1115 $filename1 = make_user_directory($user->id, true) . "/f1.jpg";
1116 $filename2 = make_user_directory($user->id, true) . "/f2.jpg";
1118 if (file_exists($filename1)) {
1119 $return['f1'] = base64_encode(file_get_contents($filename1));
1121 if (file_exists($filename2)) {
1122 $return['f2'] = base64_encode(file_get_contents($filename2));
1131 * Returns the theme information and logo url as strings.
1133 * @return string The theme info
1135 function fetch_theme_info() {
1138 $themename = "$CFG->theme";
1139 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1141 $return['themename'] = $themename;
1142 $return['logourl'] = $logourl;
1147 * Determines if an MNET host is providing the nominated service.
1149 * @param int $mnethostid The id of the remote host
1150 * @param string $servicename The name of the service
1151 * @return bool Whether the service is available on the remote host
1153 function has_service($mnethostid, $servicename) {
1158 svc.id as serviceid,
1167 {mnet_host2service} h2s
1170 h.id = h2s.hostid AND
1172 h2s.serviceid = svc.id AND
1174 h2s.subscribe = '1'";
1176 return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1180 * Checks the MNET access control table to see if the username/mnethost
1181 * is permitted to login to this moodle.
1183 * @param string $username The username
1184 * @param int $mnethostid The id of the remote mnethost
1185 * @return bool Whether the user can login from the remote host
1187 function can_login_remotely($username, $mnethostid) {
1190 $accessctrl = 'allow';
1191 $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1192 if (!empty($aclrecord)) {
1193 $accessctrl = $aclrecord->accessctrl;
1195 return $accessctrl == 'allow';
1198 function logoutpage_hook() {
1199 global $USER, $CFG, $redirect, $DB;
1201 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1202 $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1203 $redirect = $host->wwwroot.'/';
1208 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1210 * @param object $logline The log information to be trimmed
1211 * @return object The passed logline object trimmed to not exceed storable limits
1213 function trim_logline ($logline) {
1214 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1216 foreach ($limits as $property => $limit) {
1217 if (isset($logline->$property)) {
1218 $logline->$property = substr($logline->$property, 0, $limit);
1226 * Returns a list of potential IdPs that this authentication plugin supports.
1227 * This is used to provide links on the login page.
1229 * @param string $wantsurl the relative url fragment the user wants to get to. You can use this to compose a returnurl, for example
1231 * @return array like:
1234 * 'url' => 'http://someurl',
1235 * 'icon' => new pix_icon(...),
1236 * 'name' => get_string('somename', 'auth_yourplugin'),
1240 function loginpage_idp_list($wantsurl) {
1242 // strip off wwwroot, since the remote site will prefix it's return url with this
1243 $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1244 if (!$hosts = $DB->get_records_sql('SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url,a.name as application
1246 JOIN {mnet_host2service} m ON h.id=m.hostid
1247 JOIN {mnet_service} s ON s.id=m.serviceid
1248 JOIN {mnet_application} a ON h.applicationid = a.id
1249 WHERE s.name=? AND h.deleted=? AND m.publish = ?',
1250 array('sso_sp', 0, 1))) {
1254 foreach ($hosts as $host) {
1256 'url' => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1257 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1258 'name' => $host->name,