Moodle release 2.3.7
[moodle.git] / auth / mnet / auth.php
blob14a42ac0fb626007a5d52da1607121e22fa364a8
1 <?php
3 /**
4 * @author Martin Dougiamas
5 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
6 * @package moodle multiauth
8 * Authentication Plugin: Moodle Network Authentication
10 * Multiple host authentication support for Moodle Network.
12 * 2006-11-01 File created.
15 if (!defined('MOODLE_INTERNAL')) {
16 die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
19 require_once($CFG->libdir.'/authlib.php');
21 /**
22 * Moodle Network authentication plugin.
24 class auth_plugin_mnet extends auth_plugin_base {
26 /**
27 * Constructor.
29 function auth_plugin_mnet() {
30 $this->authtype = 'mnet';
31 $this->config = get_config('auth_mnet');
32 $this->mnet = get_mnet_environment();
35 /**
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");
48 /**
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));
77 // extra special ones
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 $fs = get_file_storage();
84 $usercontext = get_context_instance(CONTEXT_USER, $user->id, MUST_EXIST);
85 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
86 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
87 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
88 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
89 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
90 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
94 $userdata['myhosts'] = array();
95 if ($courses = enrol_get_users_courses($user->id, false)) {
96 $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
99 $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
100 COUNT(c.id) AS count
101 FROM {mnetservice_enrol_courses} c
102 JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
103 JOIN {mnet_host} h ON h.id = c.hostid
104 WHERE e.userid = ? AND c.hostid = ?
105 GROUP BY h.name, h.wwwroot, h.id";
107 if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) {
108 foreach($courses as $course) {
109 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
113 return $userdata;
117 * Generate a random string for use as an RPC session token.
119 function generate_token() {
120 return sha1(str_shuffle('' . mt_rand() . time()));
124 * Starts an RPC jump session and returns the jump redirect URL.
126 * @param int $mnethostid id of the mnet host to jump to
127 * @param string $wantsurl url to redirect to after the jump (usually on remote system)
128 * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
129 * rather than somewhere inside *its* wwwroot
131 function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
132 global $CFG, $USER, $DB;
133 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
135 if (session_is_loggedinas()) {
136 print_error('notpermittedtojumpas', 'mnet');
139 // check remote login permissions
140 if (! has_capability('moodle/site:mnetlogintoremote', get_system_context())
141 or is_mnet_remote_user($USER)
142 or isguestuser()
143 or !isloggedin()) {
144 print_error('notpermittedtojump', 'mnet');
147 // check for SSO publish permission first
148 if ($this->has_service($mnethostid, 'sso_sp') == false) {
149 print_error('hostnotconfiguredforsso', 'mnet');
152 // set RPC timeout to 30 seconds if not configured
153 if (empty($this->config->rpc_negotiation_timeout)) {
154 $this->config->rpc_negotiation_timeout = 30;
155 set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
158 // get the host info
159 $mnet_peer = new mnet_peer();
160 $mnet_peer->set_id($mnethostid);
162 // set up the session
163 $mnet_session = $DB->get_record('mnet_session',
164 array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
165 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
166 if ($mnet_session == false) {
167 $mnet_session = new stdClass();
168 $mnet_session->mnethostid = $mnethostid;
169 $mnet_session->userid = $USER->id;
170 $mnet_session->username = $USER->username;
171 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
172 $mnet_session->token = $this->generate_token();
173 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
174 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
175 $mnet_session->session_id = session_id();
176 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
177 } else {
178 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
179 $mnet_session->token = $this->generate_token();
180 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
181 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
182 $mnet_session->session_id = session_id();
183 $DB->update_record('mnet_session', $mnet_session);
186 // construct the redirection URL
187 //$transport = mnet_get_protocol($mnet_peer->transport);
188 $wantsurl = urlencode($wantsurl);
189 $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
190 if ($wantsurlbackhere) {
191 $url .= '&remoteurl=1';
194 return $url;
198 * This function confirms the remote (ID provider) host's mnet session
199 * by communicating the token and UA over the XMLRPC transport layer, and
200 * returns the local user record on success.
202 * @param string $token The random session token.
203 * @param mnet_peer $remotepeer The ID provider mnet_peer object.
204 * @return array The local user record.
206 function confirm_mnet_session($token, $remotepeer) {
207 global $CFG, $DB;
208 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
209 require_once $CFG->libdir . '/gdlib.php';
211 // verify the remote host is configured locally before attempting RPC call
212 if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
213 print_error('notpermittedtoland', 'mnet');
216 // set up the RPC request
217 $mnetrequest = new mnet_xmlrpc_client();
218 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
220 // set $token and $useragent parameters
221 $mnetrequest->add_param($token);
222 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
224 // Thunderbirds are go! Do RPC call and store response
225 if ($mnetrequest->send($remotepeer) === true) {
226 $remoteuser = (object) $mnetrequest->response;
227 } else {
228 foreach ($mnetrequest->error as $errormessage) {
229 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
230 if($code == 702) {
231 $site = get_site();
232 print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname));
233 exit;
235 $message .= "ERROR $code:<br/>$errormessage<br/>";
237 print_error("rpcerror", '', '', $message);
239 unset($mnetrequest);
241 if (empty($remoteuser) or empty($remoteuser->username)) {
242 print_error('unknownerror', 'mnet');
243 exit;
246 if (user_not_fully_set_up($remoteuser)) {
247 print_error('notenoughidpinfo', 'mnet');
248 exit;
251 $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
253 $remoteuser->auth = 'mnet';
254 $remoteuser->wwwroot = $remotepeer->wwwroot;
256 // the user may roam from Moodle 1.x where lang has _utf8 suffix
257 // also, make sure that the lang is actually installed, otherwise set site default
258 if (isset($remoteuser->lang)) {
259 $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
261 if (empty($remoteuser->lang)) {
262 if (!empty($CFG->lang)) {
263 $remoteuser->lang = $CFG->lang;
264 } else {
265 $remoteuser->lang = 'en';
268 $firsttime = false;
270 // get the local record for the remote user
271 $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
273 // add the remote user to the database if necessary, and if allowed
274 // TODO: refactor into a separate function
275 if (empty($localuser) || ! $localuser->id) {
277 if (empty($this->config->auto_add_remote_users)) {
278 print_error('nolocaluser', 'mnet');
279 } See MDL-21327 for why this is commented out
281 $remoteuser->mnethostid = $remotehost->id;
282 $remoteuser->firstaccess = time(); // First time user in this server, grab it here
283 $remoteuser->confirmed = 1;
285 $remoteuser->id = $DB->insert_record('user', $remoteuser);
286 $firsttime = true;
287 $localuser = $remoteuser;
290 // check sso access control list for permission first
291 if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
292 print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
295 $fs = get_file_storage();
297 // update the local user record with remote user data
298 foreach ((array) $remoteuser as $key => $val) {
300 if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
301 // update the user picture if there is a newer verion at the identity provider
302 $usercontext = get_context_instance(CONTEXT_USER, $localuser->id, MUST_EXIST);
303 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
304 $localtimemodified = $usericonfile->get_timemodified();
305 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
306 $localtimemodified = $usericonfile->get_timemodified();
307 } else {
308 $localtimemodified = 0;
311 if (!empty($val) and $localtimemodified < $val) {
312 mnet_debug('refetching the user picture from the identity provider host');
313 $fetchrequest = new mnet_xmlrpc_client();
314 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
315 $fetchrequest->add_param($localuser->username);
316 if ($fetchrequest->send($remotepeer) === true) {
317 if (strlen($fetchrequest->response['f1']) > 0) {
318 $imagefilename = $CFG->tempdir . '/mnet-usericon-' . $localuser->id;
319 $imagecontents = base64_decode($fetchrequest->response['f1']);
320 file_put_contents($imagefilename, $imagecontents);
321 if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
322 $localuser->picture = $newrev;
324 unlink($imagefilename);
326 // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
327 // the mimetype information provided is ignored and the type of the file is detected
328 // by process_new_icon()
333 if($key == 'myhosts') {
334 $localuser->mnet_foreign_host_array = array();
335 foreach($val as $rhost) {
336 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
337 $url = clean_param($rhost['url'], PARAM_URL);
338 $count = clean_param($rhost['count'], PARAM_INT);
339 $url_is_local = stristr($url , $CFG->wwwroot);
340 if (!empty($name) && !empty($count) && empty($url_is_local)) {
341 $localuser->mnet_foreign_host_array[] = array('name' => $name,
342 'url' => $url,
343 'count' => $count);
348 $localuser->{$key} = $val;
351 $localuser->mnethostid = $remotepeer->id;
352 if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
353 $localuser->firstaccess = time();
356 $DB->update_record('user', $localuser);
358 if (!$firsttime) {
359 // repeat customer! let the IDP know about enrolments
360 // we have for this user.
361 // set up the RPC request
362 $mnetrequest = new mnet_xmlrpc_client();
363 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
365 // pass username and an assoc array of "my courses"
366 // with info so that the IDP can maintain mnetservice_enrol_enrolments
367 $mnetrequest->add_param($remoteuser->username);
368 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
369 $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
370 if (is_array($courses) && !empty($courses)) {
371 // Second request to do the JOINs that we'd have done
372 // inside enrol_get_users_courses() if we had been allowed
373 $sql = "SELECT c.id,
374 cc.name AS cat_name, cc.description AS cat_description
375 FROM {course} c
376 JOIN {course_categories} cc ON c.category = cc.id
377 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
378 $extra = $DB->get_records_sql($sql);
380 $keys = array_keys($courses);
381 $studentroles = get_archetype_roles('student');
382 if (!empty($studentroles)) {
383 $defaultrole = reset($studentroles);
384 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
385 foreach ($keys AS $id) {
386 if ($courses[$id]->visible == 0) {
387 unset($courses[$id]);
388 continue;
390 $courses[$id]->cat_id = $courses[$id]->category;
391 $courses[$id]->defaultroleid = $defaultrole->id;
392 unset($courses[$id]->category);
393 unset($courses[$id]->visible);
395 $courses[$id]->cat_name = $extra[$id]->cat_name;
396 $courses[$id]->cat_description = $extra[$id]->cat_description;
397 $courses[$id]->defaultrolename = $defaultrole->name;
398 // coerce to array
399 $courses[$id] = (array)$courses[$id];
401 } else {
402 throw new moodle_exception('unknownrole', 'error', '', 'student');
404 } else {
405 // if the array is empty, send it anyway
406 // we may be clearing out stale entries
407 $courses = array();
409 $mnetrequest->add_param($courses);
411 // Call 0800-RPC Now! -- we don't care too much if it fails
412 // as it's just informational.
413 if ($mnetrequest->send($remotepeer) === false) {
414 // error_log(print_r($mnetrequest->error,1));
418 return $localuser;
423 * creates (or updates) the mnet session once
424 * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
426 * @param stdclass $user the local user (must exist already
427 * @param string $token the jump/land token
428 * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
430 public function update_mnet_session($user, $token, $remotepeer) {
431 global $DB;
432 $session_gc_maxlifetime = 1440;
433 if (isset($user->session_gc_maxlifetime)) {
434 $session_gc_maxlifetime = $user->session_gc_maxlifetime;
436 if (!$mnet_session = $DB->get_record('mnet_session',
437 array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
438 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
439 $mnet_session = new stdClass();
440 $mnet_session->mnethostid = $remotepeer->id;
441 $mnet_session->userid = $user->id;
442 $mnet_session->username = $user->username;
443 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
444 $mnet_session->token = $token; // Needed to support simultaneous sessions
445 // and preserving DB rec uniqueness
446 $mnet_session->confirm_timeout = time();
447 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
448 $mnet_session->session_id = session_id();
449 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
450 } else {
451 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
452 $DB->update_record('mnet_session', $mnet_session);
459 * Invoke this function _on_ the IDP to update it with enrolment info local to
460 * the SP right after calling user_authorise()
462 * Normally called by the SP after calling user_authorise()
464 * @param string $username The username
465 * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses
466 * @return bool
468 function update_enrolments($username, $courses) {
469 global $CFG, $DB;
470 $remoteclient = get_mnet_remote_client();
472 if (empty($username) || !is_array($courses)) {
473 return false;
475 // make sure it is a user we have an in active session
476 // with that host...
477 $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
478 $userid = null;
479 foreach ($mnetsessions as $mnetsession) {
480 if (is_null($userid)) {
481 $userid = $mnetsession->userid;
482 continue;
484 if ($userid != $mnetsession->userid) {
485 throw new mnet_server_exception(3, 'authfail_usermismatch');
489 if (empty($courses)) { // no courses? clear out quickly
490 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
491 return true;
494 // IMPORTANT: Ask for remoteid as the first element in the query, so
495 // that the array that comes back is indexed on the same field as the
496 // array that we have received from the remote client
497 $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
498 c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
499 e.id AS enrolmentid
500 FROM {mnetservice_enrol_courses} c
501 LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
502 WHERE e.userid = ? AND c.hostid = ?";
504 $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
506 $local_courseid_array = array();
507 foreach($courses as $ix => $course) {
509 $course['remoteid'] = $course['id'];
510 $course['hostid'] = (int)$remoteclient->id;
511 $userisregd = false;
513 // if we do not have the the information about the remote course, it is not available
514 // to us for remote enrolment - skip
515 if (array_key_exists($course['remoteid'], $currentcourses)) {
516 // Pointer to current course:
517 $currentcourse =& $currentcourses[$course['remoteid']];
518 // We have a record - is it up-to-date?
519 $course['id'] = $currentcourse->id;
521 $saveflag = false;
523 foreach($course as $key => $value) {
524 if ($currentcourse->$key != $value) {
525 $saveflag = true;
526 $currentcourse->$key = $value;
530 if ($saveflag) {
531 $DB->update_record('mnetervice_enrol_courses', $currentcourse);
534 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
535 $userisregd = true;
537 } else {
538 unset ($courses[$ix]);
539 continue;
542 // By this point, we should always have a $dataObj->id
543 $local_courseid_array[] = $course['id'];
545 // Do we have a record for this assignment?
546 if ($userisregd) {
547 // Yes - we know about this one already
548 // We don't want to do updates because the new data is probably
549 // 'less complete' than the data we have.
550 } else {
551 // No - create a record
552 $assignObj = new stdClass();
553 $assignObj->userid = $userid;
554 $assignObj->hostid = (int)$remoteclient->id;
555 $assignObj->remotecourseid = $course['remoteid'];
556 $assignObj->rolename = $course['defaultrolename'];
557 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
561 // Clean up courses that the user is no longer enrolled in.
562 if (!empty($local_courseid_array)) {
563 $local_courseid_string = implode(', ', $local_courseid_array);
564 $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
565 $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
569 function prevent_local_passwords() {
570 return true;
574 * Returns true if this authentication plugin is 'internal'.
576 * @return bool
578 function is_internal() {
579 return false;
583 * Returns true if this authentication plugin can change the user's
584 * password.
586 * @return bool
588 function can_change_password() {
589 //TODO: it should be able to redirect, right?
590 return false;
594 * Returns the URL for changing the user's pw, or false if the default can
595 * be used.
597 * @return moodle_url
599 function change_password_url() {
600 return null;
604 * Prints a form for configuring this authentication plugin.
606 * This function is called from admin/auth.php, and outputs a full page with
607 * a form for configuring this plugin.
609 * @param object $config
610 * @param object $err
611 * @param array $user_fields
613 function config_form($config, $err, $user_fields) {
614 global $CFG, $DB;
616 $query = "
617 SELECT
618 h.id,
619 h.name as hostname,
620 h.wwwroot,
621 h2idp.publish as idppublish,
622 h2idp.subscribe as idpsubscribe,
623 idp.name as idpname,
624 h2sp.publish as sppublish,
625 h2sp.subscribe as spsubscribe,
626 sp.name as spname
627 FROM
628 {mnet_host} h
629 LEFT JOIN
630 {mnet_host2service} h2idp
632 (h.id = h2idp.hostid AND
633 (h2idp.publish = 1 OR
634 h2idp.subscribe = 1))
635 INNER JOIN
636 {mnet_service} idp
638 (h2idp.serviceid = idp.id AND
639 idp.name = 'sso_idp')
640 LEFT JOIN
641 {mnet_host2service} h2sp
643 (h.id = h2sp.hostid AND
644 (h2sp.publish = 1 OR
645 h2sp.subscribe = 1))
646 INNER JOIN
647 {mnet_service} sp
649 (h2sp.serviceid = sp.id AND
650 sp.name = 'sso_sp')
651 WHERE
652 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
653 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
654 h.id != ?
655 ORDER BY
656 h.name ASC";
658 $id_providers = array();
659 $service_providers = array();
660 if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
661 foreach($resultset as $hostservice) {
662 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
663 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
665 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
666 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
671 include "config.html";
675 * Processes and stores configuration data for this authentication plugin.
677 function process_config($config) {
678 // set to defaults if undefined
679 if (!isset ($config->rpc_negotiation_timeout)) {
680 $config->rpc_negotiation_timeout = '30';
683 if (!isset ($config->auto_add_remote_users)) {
684 $config->auto_add_remote_users = '0';
685 } See MDL-21327 for why this is commented out
686 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth_mnet');
689 // save settings
690 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
692 return true;
696 * Poll the IdP server to let it know that a user it has authenticated is still
697 * online
699 * @return void
701 function keepalive_client() {
702 global $CFG, $DB;
703 $cutoff = time() - 300; // TODO - find out what the remote server's session
704 // cutoff is, and preempt that
706 $sql = "
707 select
709 username,
710 mnethostid
711 from
712 {user}
713 where
714 lastaccess > ? AND
715 mnethostid != ?
716 order by
717 mnethostid";
719 $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
721 if ($immigrants == false) {
722 return true;
725 $usersArray = array();
726 foreach($immigrants as $immigrant) {
727 $usersArray[$immigrant->mnethostid][] = $immigrant->username;
730 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
731 foreach($usersArray as $mnethostid => $users) {
732 $mnet_peer = new mnet_peer();
733 $mnet_peer->set_id($mnethostid);
735 $mnet_request = new mnet_xmlrpc_client();
736 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
738 // set $token and $useragent parameters
739 $mnet_request->add_param($users);
741 if ($mnet_request->send($mnet_peer) === true) {
742 if (!isset($mnet_request->response['code'])) {
743 debugging("Server side error has occured on host $mnethostid");
744 continue;
745 } elseif ($mnet_request->response['code'] > 0) {
746 debugging($mnet_request->response['message']);
749 if (!isset($mnet_request->response['last log id'])) {
750 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
751 continue;
753 } else {
754 debugging("Server side error has occured on host $mnethostid: " .
755 join("\n", $mnet_request->error));
756 break;
758 $mnethostlogssql = "
759 SELECT
760 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
761 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
762 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
763 c.modinfo
764 FROM
766 SELECT
767 l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
768 l.action, l.url, l.info, u.username
769 FROM
770 {user} u
771 INNER JOIN {log} l on l.userid = u.id
772 WHERE
773 u.mnethostid = ?
774 AND l.id > ?
775 ORDER BY remoteid ASC
776 LIMIT 500
777 ) mhostlogs
778 INNER JOIN {course} c on c.id = mhostlogs.course
779 ORDER by mhostlogs.remoteid ASC";
781 $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
783 if ($mnethostlogs == false) {
784 continue;
787 $processedlogs = array();
789 foreach($mnethostlogs as $hostlog) {
790 // Extract the name of the relevant module instance from the
791 // course modinfo if possible.
792 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
793 $modinfo = unserialize($hostlog->modinfo);
794 unset($hostlog->modinfo);
795 $modulearray = array();
796 foreach($modinfo as $module) {
797 $modulearray[$module->cm] = $module->name;
799 $hostlog->resource_name = $modulearray[$hostlog->cmid];
800 } else {
801 $hostlog->resource_name = '';
804 $processedlogs[] = array (
805 'remoteid' => $hostlog->remoteid,
806 'time' => $hostlog->time,
807 'userid' => $hostlog->userid,
808 'ip' => $hostlog->ip,
809 'course' => $hostlog->course,
810 'coursename' => $hostlog->coursename,
811 'module' => $hostlog->module,
812 'cmid' => $hostlog->cmid,
813 'action' => $hostlog->action,
814 'url' => $hostlog->url,
815 'info' => $hostlog->info,
816 'resource_name' => $hostlog->resource_name,
817 'username' => $hostlog->username
821 unset($hostlog);
823 $mnet_request = new mnet_xmlrpc_client();
824 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
826 // set $token and $useragent parameters
827 $mnet_request->add_param($processedlogs);
829 if ($mnet_request->send($mnet_peer) === true) {
830 if ($mnet_request->response['code'] > 0) {
831 debugging($mnet_request->response['message']);
833 } else {
834 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
840 * Receives an array of log entries from an SP and adds them to the mnet_log
841 * table
843 * @param array $array An array of usernames
844 * @return string "All ok" or an error message
846 function refresh_log($array) {
847 global $CFG, $DB;
848 $remoteclient = get_mnet_remote_client();
850 // We don't want to output anything to the client machine
851 $start = ob_start();
853 $returnString = '';
854 $transaction = $DB->start_delegated_transaction();
855 $useridarray = array();
857 foreach($array as $logEntry) {
858 $logEntryObj = (object)$logEntry;
859 $logEntryObj->hostid = $remoteclient->id;
861 if (isset($useridarray[$logEntryObj->username])) {
862 $logEntryObj->userid = $useridarray[$logEntryObj->username];
863 } else {
864 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
865 if ($logEntryObj->userid == false) {
866 $logEntryObj->userid = 0;
868 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
871 unset($logEntryObj->username);
873 $logEntryObj = $this->trim_logline($logEntryObj);
874 $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
876 if ($insertok) {
877 $remoteclient->last_log_id = $logEntryObj->remoteid;
878 } else {
879 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
882 $remoteclient->commit();
883 $transaction->allow_commit();
885 $end = ob_end_clean();
887 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
888 return array('code' => 1, 'message' => $returnString);
892 * Receives an array of usernames from a remote machine and prods their
893 * sessions to keep them alive
895 * @param array $array An array of usernames
896 * @return string "All ok" or an error message
898 function keepalive_server($array) {
899 global $CFG, $DB;
900 $remoteclient = get_mnet_remote_client();
902 // We don't want to output anything to the client machine
903 $start = ob_start();
905 // We'll get session records in batches of 30
906 $superArray = array_chunk($array, 30);
908 $returnString = '';
910 foreach($superArray as $subArray) {
911 $subArray = array_values($subArray);
912 $instring = "('".implode("', '",$subArray)."')";
913 $query = "select id, session_id, username from {mnet_session} where username in $instring";
914 $results = $DB->get_records_sql($query);
916 if ($results == false) {
917 // We seem to have a username that breaks our query:
918 // TODO: Handle this error appropriately
919 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
920 } else {
921 foreach($results as $emigrant) {
922 session_touch($emigrant->session_id);
927 $end = ob_end_clean();
929 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
930 return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
934 * Cron function will be called automatically by cron.php every 5 minutes
936 * @return void
938 function cron() {
939 global $DB;
941 // run the keepalive client
942 $this->keepalive_client();
944 // admin/cron.php should have run srand for us
945 $random100 = rand(0,100);
946 if ($random100 < 10) { // Approximately 10% of the time.
947 // nuke olden sessions
948 $longtime = time() - (1 * 3600 * 24);
949 $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
954 * Cleanup any remote mnet_sessions, kill the local mnet_session data
956 * This is called by require_logout in moodlelib
958 * @return void
960 function prelogout_hook() {
961 global $CFG, $USER;
963 if (!is_enabled_auth('mnet')) {
964 return;
967 // If the user is local to this Moodle:
968 if ($USER->mnethostid == $this->mnet->id) {
969 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
971 // Else the user has hit 'logout' at a Service Provider Moodle:
972 } else {
973 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
979 * The SP uses this function to kill the session on the parent IdP
981 * @param string $username Username for session to kill
982 * @param string $useragent SHA1 hash of user agent to look for
983 * @return string A plaintext report of what has happened
985 function kill_parent($username, $useragent) {
986 global $CFG, $USER, $DB;
988 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
989 $sql = "
990 select
992 from
993 {mnet_session} s
994 where
995 s.username = ? AND
996 s.useragent = ? AND
997 s.mnethostid = ?";
999 $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
1001 $ignore = $DB->delete_records('mnet_session',
1002 array('username'=>$username,
1003 'useragent'=>$useragent,
1004 'mnethostid'=>$USER->mnethostid));
1006 if (false != $mnetsessions) {
1007 $mnet_peer = new mnet_peer();
1008 $mnet_peer->set_id($USER->mnethostid);
1010 $mnet_request = new mnet_xmlrpc_client();
1011 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1013 // set $token and $useragent parameters
1014 $mnet_request->add_param($username);
1015 $mnet_request->add_param($useragent);
1016 if ($mnet_request->send($mnet_peer) === false) {
1017 debugging(join("\n", $mnet_request->error));
1018 return false;
1022 return true;
1026 * The IdP uses this function to kill child sessions on other hosts
1028 * @param string $username Username for session to kill
1029 * @param string $useragent SHA1 hash of user agent to look for
1030 * @return string A plaintext report of what has happened
1032 function kill_children($username, $useragent) {
1033 global $CFG, $USER, $DB;
1034 $remoteclient = null;
1035 if (defined('MNET_SERVER')) {
1036 $remoteclient = get_mnet_remote_client();
1038 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1040 $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1042 $returnstring = '';
1044 $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1046 if (false == $mnetsessions) {
1047 $returnstring .= "Could find no remote sessions\n";
1048 $mnetsessions = array();
1051 foreach($mnetsessions as $mnetsession) {
1052 // If this script is being executed by a remote peer, that means the user has clicked
1053 // logout on that peer, and the session on that peer can be deleted natively.
1054 // Skip over it.
1055 if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1056 continue;
1058 $returnstring .= "Deleting session\n";
1060 $mnet_peer = new mnet_peer();
1061 $mnet_peer->set_id($mnetsession->mnethostid);
1063 $mnet_request = new mnet_xmlrpc_client();
1064 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1066 // set $token and $useragent parameters
1067 $mnet_request->add_param($username);
1068 $mnet_request->add_param($useragent);
1069 if ($mnet_request->send($mnet_peer) === false) {
1070 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1071 join("\n", $mnet_request->error));
1075 $ignore = $DB->delete_records('mnet_session',
1076 array('useragent'=>$useragent, 'userid'=>$userid));
1078 if (isset($remoteclient) && isset($remoteclient->id)) {
1079 session_kill_user($userid);
1081 return $returnstring;
1085 * When the IdP requests that child sessions are terminated,
1086 * this function will be called on each of the child hosts. The machine that
1087 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1089 * @param string $username Username for session to kill
1090 * @param string $useragent SHA1 hash of user agent to look for
1091 * @return bool True on success
1093 function kill_child($username, $useragent) {
1094 global $CFG, $DB;
1095 $remoteclient = get_mnet_remote_client();
1096 $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1097 $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1098 if (false != $session) {
1099 session_kill($session->session_id);
1100 return true;
1102 return false;
1106 * To delete a host, we must delete all current sessions that users from
1107 * that host are currently engaged in.
1109 * @param string $sessionidarray An array of session hashes
1110 * @return bool True on success
1112 function end_local_sessions(&$sessionArray) {
1113 global $CFG;
1114 if (is_array($sessionArray)) {
1115 while($session = array_pop($sessionArray)) {
1116 session_kill($session->session_id);
1118 return true;
1120 return false;
1124 * Returns the user's profile image info
1126 * If the user exists and has a profile picture, the returned array will contain keys:
1127 * f1 - the content of the default 100x100px image
1128 * f1_mimetype - the mimetype of the f1 file
1129 * f2 - the content of the 35x35px variant of the image
1130 * f2_mimetype - the mimetype of the f2 file
1132 * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1134 * @see process_new_icon()
1135 * @uses mnet_remote_client callable via MNet XML-RPC
1136 * @param int $userid The id of the user
1137 * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1139 function fetch_user_image($username) {
1140 global $CFG, $DB;
1142 if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
1143 $fs = get_file_storage();
1144 $usercontext = get_context_instance(CONTEXT_USER, $user->id, MUST_EXIST);
1145 $return = array();
1146 if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
1147 $return['f1'] = base64_encode($f1->get_content());
1148 $return['f1_mimetype'] = $f1->get_mimetype();
1149 } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
1150 $return['f1'] = base64_encode($f1->get_content());
1151 $return['f1_mimetype'] = $f1->get_mimetype();
1153 if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
1154 $return['f2'] = base64_encode($f2->get_content());
1155 $return['f2_mimetype'] = $f2->get_mimetype();
1156 } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
1157 $return['f2'] = base64_encode($f2->get_content());
1158 $return['f2_mimetype'] = $f2->get_mimetype();
1160 return $return;
1162 return false;
1166 * Returns the theme information and logo url as strings.
1168 * @return string The theme info
1170 function fetch_theme_info() {
1171 global $CFG;
1173 $themename = "$CFG->theme";
1174 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1176 $return['themename'] = $themename;
1177 $return['logourl'] = $logourl;
1178 return $return;
1182 * Determines if an MNET host is providing the nominated service.
1184 * @param int $mnethostid The id of the remote host
1185 * @param string $servicename The name of the service
1186 * @return bool Whether the service is available on the remote host
1188 function has_service($mnethostid, $servicename) {
1189 global $CFG, $DB;
1191 $sql = "
1192 SELECT
1193 svc.id as serviceid,
1194 svc.name,
1195 svc.description,
1196 svc.offer,
1197 svc.apiversion,
1198 h2s.id as h2s_id
1199 FROM
1200 {mnet_host} h,
1201 {mnet_service} svc,
1202 {mnet_host2service} h2s
1203 WHERE
1204 h.deleted = '0' AND
1205 h.id = h2s.hostid AND
1206 h2s.hostid = ? AND
1207 h2s.serviceid = svc.id AND
1208 svc.name = ? AND
1209 h2s.subscribe = '1'";
1211 return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1215 * Checks the MNET access control table to see if the username/mnethost
1216 * is permitted to login to this moodle.
1218 * @param string $username The username
1219 * @param int $mnethostid The id of the remote mnethost
1220 * @return bool Whether the user can login from the remote host
1222 function can_login_remotely($username, $mnethostid) {
1223 global $DB;
1225 $accessctrl = 'allow';
1226 $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1227 if (!empty($aclrecord)) {
1228 $accessctrl = $aclrecord->accessctrl;
1230 return $accessctrl == 'allow';
1233 function logoutpage_hook() {
1234 global $USER, $CFG, $redirect, $DB;
1236 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1237 $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1238 $redirect = $host->wwwroot.'/';
1243 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1245 * @param object $logline The log information to be trimmed
1246 * @return object The passed logline object trimmed to not exceed storable limits
1248 function trim_logline ($logline) {
1249 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1250 'url' => 255);
1251 foreach ($limits as $property => $limit) {
1252 if (isset($logline->$property)) {
1253 $logline->$property = substr($logline->$property, 0, $limit);
1257 return $logline;
1261 * Returns a list of potential IdPs that this authentication plugin supports.
1262 * This is used to provide links on the login page.
1264 * @param string $wantsurl the relative url fragment the user wants to get to. You can use this to compose a returnurl, for example
1266 * @return array like:
1267 * array(
1268 * array(
1269 * 'url' => 'http://someurl',
1270 * 'icon' => new pix_icon(...),
1271 * 'name' => get_string('somename', 'auth_yourplugin'),
1272 * ),
1275 function loginpage_idp_list($wantsurl) {
1276 global $DB, $CFG;
1278 // strip off wwwroot, since the remote site will prefix it's return url with this
1279 $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1281 $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1282 FROM {mnet_host} h
1283 JOIN {mnet_host2service} m ON h.id = m.hostid
1284 JOIN {mnet_service} s ON s.id = m.serviceid
1285 JOIN {mnet_application} a ON h.applicationid = a.id
1286 WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1287 $params = array('sso_sp', 0, 1);
1289 if (!empty($CFG->mnet_all_hosts_id)) {
1290 $sql .= " AND h.id <> ?";
1291 $params[] = $CFG->mnet_all_hosts_id;
1294 if (!$hosts = $DB->get_records_sql($sql, $params)) {
1295 return array();
1298 $idps = array();
1299 foreach ($hosts as $host) {
1300 $idps[] = array(
1301 'url' => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1302 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1303 'name' => $host->name,
1306 return $idps;