Merge branch 'MDL-4633_m20' of git://github.com/rwijaya/moodle
[moodle.git] / auth / mnet / auth.php
blob4008d3858dfa08f7236e5665ad7eb31b35d1f67f
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 // check remote login permissions
136 if (! has_capability('moodle/site:mnetlogintoremote', get_system_context())
137 or is_mnet_remote_user($USER)
138 or isguestuser()
139 or !isloggedin()) {
140 print_error('notpermittedtojump', 'mnet');
143 // check for SSO publish permission first
144 if ($this->has_service($mnethostid, 'sso_sp') == false) {
145 print_error('hostnotconfiguredforsso', 'mnet');
148 // set RPC timeout to 30 seconds if not configured
149 if (empty($this->config->rpc_negotiation_timeout)) {
150 $this->config->rpc_negotiation_timeout = 30;
151 set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
154 // get the host info
155 $mnet_peer = new mnet_peer();
156 $mnet_peer->set_id($mnethostid);
158 // set up the session
159 $mnet_session = $DB->get_record('mnet_session',
160 array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
161 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
162 if ($mnet_session == false) {
163 $mnet_session = new stdClass();
164 $mnet_session->mnethostid = $mnethostid;
165 $mnet_session->userid = $USER->id;
166 $mnet_session->username = $USER->username;
167 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
168 $mnet_session->token = $this->generate_token();
169 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
170 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
171 $mnet_session->session_id = session_id();
172 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
173 } else {
174 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
175 $mnet_session->token = $this->generate_token();
176 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
177 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
178 $mnet_session->session_id = session_id();
179 $DB->update_record('mnet_session', $mnet_session);
182 // construct the redirection URL
183 //$transport = mnet_get_protocol($mnet_peer->transport);
184 $wantsurl = urlencode($wantsurl);
185 $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
186 if ($wantsurlbackhere) {
187 $url .= '&remoteurl=1';
190 return $url;
194 * This function confirms the remote (ID provider) host's mnet session
195 * by communicating the token and UA over the XMLRPC transport layer, and
196 * returns the local user record on success.
198 * @param string $token The random session token.
199 * @param mnet_peer $remotepeer The ID provider mnet_peer object.
200 * @return array The local user record.
202 function confirm_mnet_session($token, $remotepeer) {
203 global $CFG, $DB;
204 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
205 require_once $CFG->libdir . '/gdlib.php';
207 // verify the remote host is configured locally before attempting RPC call
208 if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
209 print_error('notpermittedtoland', 'mnet');
212 // set up the RPC request
213 $mnetrequest = new mnet_xmlrpc_client();
214 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
216 // set $token and $useragent parameters
217 $mnetrequest->add_param($token);
218 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
220 // Thunderbirds are go! Do RPC call and store response
221 if ($mnetrequest->send($remotepeer) === true) {
222 $remoteuser = (object) $mnetrequest->response;
223 } else {
224 foreach ($mnetrequest->error as $errormessage) {
225 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
226 if($code == 702) {
227 $site = get_site();
228 print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname));
229 exit;
231 $message .= "ERROR $code:<br/>$errormessage<br/>";
233 print_error("rpcerror", '', '', $message);
235 unset($mnetrequest);
237 if (empty($remoteuser) or empty($remoteuser->username)) {
238 print_error('unknownerror', 'mnet');
239 exit;
242 if (user_not_fully_set_up($remoteuser)) {
243 print_error('notenoughidpinfo', 'mnet');
244 exit;
247 $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
249 $remoteuser->auth = 'mnet';
250 $remoteuser->wwwroot = $remotepeer->wwwroot;
252 // the user may roam from Moodle 1.x where lang has _utf8 suffix
253 // also, make sure that the lang is actually installed, otherwise set site default
254 if (isset($remoteuser->lang)) {
255 $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
257 if (empty($remoteuser->lang)) {
258 if (!empty($CFG->lang)) {
259 $remoteuser->lang = $CFG->lang;
260 } else {
261 $remoteuser->lang = 'en';
264 $firsttime = false;
266 // get the local record for the remote user
267 $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
269 // add the remote user to the database if necessary, and if allowed
270 // TODO: refactor into a separate function
271 if (empty($localuser) || ! $localuser->id) {
273 if (empty($this->config->auto_add_remote_users)) {
274 print_error('nolocaluser', 'mnet');
275 } See MDL-21327 for why this is commented out
277 $remoteuser->mnethostid = $remotehost->id;
278 $remoteuser->firstaccess = time(); // First time user in this server, grab it here
280 $remoteuser->id = $DB->insert_record('user', $remoteuser);
281 $firsttime = true;
282 $localuser = $remoteuser;
285 // check sso access control list for permission first
286 if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
287 print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
290 $fs = get_file_storage();
292 // update the local user record with remote user data
293 foreach ((array) $remoteuser as $key => $val) {
295 if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
296 // update the user picture if there is a newer verion at the identity provider
297 $usercontext = get_context_instance(CONTEXT_USER, $localuser->id, MUST_EXIST);
298 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
299 $localtimemodified = $usericonfile->get_timemodified();
300 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
301 $localtimemodified = $usericonfile->get_timemodified();
302 } else {
303 $localtimemodified = 0;
306 if (!empty($val) and $localtimemodified < $val) {
307 mnet_debug('refetching the user picture from the identity provider host');
308 $fetchrequest = new mnet_xmlrpc_client();
309 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
310 $fetchrequest->add_param($localuser->username);
311 if ($fetchrequest->send($remotepeer) === true) {
312 if (strlen($fetchrequest->response['f1']) > 0) {
313 $imagefilename = $CFG->dataroot . '/temp/mnet-usericon-' . $localuser->id;
314 $imagecontents = base64_decode($fetchrequest->response['f1']);
315 file_put_contents($imagefilename, $imagecontents);
316 if (process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
317 $localuser->picture = 1;
319 unlink($imagefilename);
321 // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
322 // the mimetype information provided is ignored and the type of the file is detected
323 // by process_new_icon()
328 if($key == 'myhosts') {
329 $localuser->mnet_foreign_host_array = array();
330 foreach($val as $rhost) {
331 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
332 $url = clean_param($rhost['url'], PARAM_URL);
333 $count = clean_param($rhost['count'], PARAM_INT);
334 $url_is_local = stristr($url , $CFG->wwwroot);
335 if (!empty($name) && !empty($count) && empty($url_is_local)) {
336 $localuser->mnet_foreign_host_array[] = array('name' => $name,
337 'url' => $url,
338 'count' => $count);
343 $localuser->{$key} = $val;
346 $localuser->mnethostid = $remotepeer->id;
347 if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
348 $localuser->firstaccess = time();
351 $DB->update_record('user', $localuser);
353 if (!$firsttime) {
354 // repeat customer! let the IDP know about enrolments
355 // we have for this user.
356 // set up the RPC request
357 $mnetrequest = new mnet_xmlrpc_client();
358 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
360 // pass username and an assoc array of "my courses"
361 // with info so that the IDP can maintain mnetservice_enrol_enrolments
362 $mnetrequest->add_param($remoteuser->username);
363 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
364 $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
365 if (is_array($courses) && !empty($courses)) {
366 // Second request to do the JOINs that we'd have done
367 // inside enrol_get_users_courses() if we had been allowed
368 $sql = "SELECT c.id,
369 cc.name AS cat_name, cc.description AS cat_description
370 FROM {course} c
371 JOIN {course_categories} cc ON c.category = cc.id
372 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
373 $extra = $DB->get_records_sql($sql);
375 $keys = array_keys($courses);
376 $defaultrole = reset(get_archetype_roles('student'));
377 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
378 foreach ($keys AS $id) {
379 if ($courses[$id]->visible == 0) {
380 unset($courses[$id]);
381 continue;
383 $courses[$id]->cat_id = $courses[$id]->category;
384 $courses[$id]->defaultroleid = $defaultrole->id;
385 unset($courses[$id]->category);
386 unset($courses[$id]->visible);
388 $courses[$id]->cat_name = $extra[$id]->cat_name;
389 $courses[$id]->cat_description = $extra[$id]->cat_description;
390 $courses[$id]->defaultrolename = $defaultrole->name;
391 // coerce to array
392 $courses[$id] = (array)$courses[$id];
394 } else {
395 // if the array is empty, send it anyway
396 // we may be clearing out stale entries
397 $courses = array();
399 $mnetrequest->add_param($courses);
401 // Call 0800-RPC Now! -- we don't care too much if it fails
402 // as it's just informational.
403 if ($mnetrequest->send($remotepeer) === false) {
404 // error_log(print_r($mnetrequest->error,1));
408 return $localuser;
413 * creates (or updates) the mnet session once
414 * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
416 * @param stdclass $user the local user (must exist already
417 * @param string $token the jump/land token
418 * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
420 public function update_mnet_session($user, $token, $remotepeer) {
421 global $DB;
422 $session_gc_maxlifetime = 1440;
423 if (isset($user->session_gc_maxlifetime)) {
424 $session_gc_maxlifetime = $user->session_gc_maxlifetime;
426 if (!$mnet_session = $DB->get_record('mnet_session',
427 array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
428 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
429 $mnet_session = new stdClass();
430 $mnet_session->mnethostid = $remotepeer->id;
431 $mnet_session->userid = $user->id;
432 $mnet_session->username = $user->username;
433 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
434 $mnet_session->token = $token; // Needed to support simultaneous sessions
435 // and preserving DB rec uniqueness
436 $mnet_session->confirm_timeout = time();
437 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
438 $mnet_session->session_id = session_id();
439 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
440 } else {
441 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
442 $DB->update_record('mnet_session', $mnet_session);
449 * Invoke this function _on_ the IDP to update it with enrolment info local to
450 * the SP right after calling user_authorise()
452 * Normally called by the SP after calling user_authorise()
454 * @param string $username The username
455 * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses
456 * @return bool
458 function update_enrolments($username, $courses) {
459 global $CFG, $DB;
460 $remoteclient = get_mnet_remote_client();
462 if (empty($username) || !is_array($courses)) {
463 return false;
465 // make sure it is a user we have an in active session
466 // with that host...
467 $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
468 $userid = null;
469 foreach ($mnetsessions as $mnetsession) {
470 if (is_null($userid)) {
471 $userid = $mnetsession->userid;
472 continue;
474 if ($userid != $mnetsession->userid) {
475 throw new mnet_server_exception(3, 'authfail_usermismatch');
479 if (empty($courses)) { // no courses? clear out quickly
480 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
481 return true;
484 // IMPORTANT: Ask for remoteid as the first element in the query, so
485 // that the array that comes back is indexed on the same field as the
486 // array that we have received from the remote client
487 $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
488 c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
489 e.id AS enrolmentid
490 FROM {mnetservice_enrol_courses} c
491 LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
492 WHERE e.userid = ? AND c.hostid = ?";
494 $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
496 $local_courseid_array = array();
497 foreach($courses as $ix => $course) {
499 $course['remoteid'] = $course['id'];
500 $course['hostid'] = (int)$remoteclient->id;
501 $userisregd = false;
503 // if we do not have the the information about the remote course, it is not available
504 // to us for remote enrolment - skip
505 if (array_key_exists($course['remoteid'], $currentcourses)) {
506 // Pointer to current course:
507 $currentcourse =& $currentcourses[$course['remoteid']];
508 // We have a record - is it up-to-date?
509 $course['id'] = $currentcourse->id;
511 $saveflag = false;
513 foreach($course as $key => $value) {
514 if ($currentcourse->$key != $value) {
515 $saveflag = true;
516 $currentcourse->$key = $value;
520 if ($saveflag) {
521 $DB->update_record('mnetervice_enrol_courses', $currentcourse);
524 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
525 $userisregd = true;
527 } else {
528 unset ($courses[$ix]);
529 continue;
532 // By this point, we should always have a $dataObj->id
533 $local_courseid_array[] = $course['id'];
535 // Do we have a record for this assignment?
536 if ($userisregd) {
537 // Yes - we know about this one already
538 // We don't want to do updates because the new data is probably
539 // 'less complete' than the data we have.
540 } else {
541 // No - create a record
542 $assignObj = new stdClass();
543 $assignObj->userid = $userid;
544 $assignObj->hostid = (int)$remoteclient->id;
545 $assignObj->remotecourseid = $course['remoteid'];
546 $assignObj->rolename = $course['defaultrolename'];
547 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
551 // Clean up courses that the user is no longer enrolled in.
552 if (!empty($local_courseid_array)) {
553 $local_courseid_string = implode(', ', $local_courseid_array);
554 $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
555 $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
559 function prevent_local_passwords() {
560 return true;
564 * Returns true if this authentication plugin is 'internal'.
566 * @return bool
568 function is_internal() {
569 return false;
573 * Returns true if this authentication plugin can change the user's
574 * password.
576 * @return bool
578 function can_change_password() {
579 //TODO: it should be able to redirect, right?
580 return false;
584 * Returns the URL for changing the user's pw, or false if the default can
585 * be used.
587 * @return moodle_url
589 function change_password_url() {
590 return null;
594 * Prints a form for configuring this authentication plugin.
596 * This function is called from admin/auth.php, and outputs a full page with
597 * a form for configuring this plugin.
599 * @param object $config
600 * @param object $err
601 * @param array $user_fields
603 function config_form($config, $err, $user_fields) {
604 global $CFG, $DB;
606 $query = "
607 SELECT
608 h.id,
609 h.name as hostname,
610 h.wwwroot,
611 h2idp.publish as idppublish,
612 h2idp.subscribe as idpsubscribe,
613 idp.name as idpname,
614 h2sp.publish as sppublish,
615 h2sp.subscribe as spsubscribe,
616 sp.name as spname
617 FROM
618 {mnet_host} h
619 LEFT JOIN
620 {mnet_host2service} h2idp
622 (h.id = h2idp.hostid AND
623 (h2idp.publish = 1 OR
624 h2idp.subscribe = 1))
625 INNER JOIN
626 {mnet_service} idp
628 (h2idp.serviceid = idp.id AND
629 idp.name = 'sso_idp')
630 LEFT JOIN
631 {mnet_host2service} h2sp
633 (h.id = h2sp.hostid AND
634 (h2sp.publish = 1 OR
635 h2sp.subscribe = 1))
636 INNER JOIN
637 {mnet_service} sp
639 (h2sp.serviceid = sp.id AND
640 sp.name = 'sso_sp')
641 WHERE
642 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
643 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
644 h.id != ?
645 ORDER BY
646 h.name ASC";
648 $id_providers = array();
649 $service_providers = array();
650 if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
651 foreach($resultset as $hostservice) {
652 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
653 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
655 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
656 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
661 include "config.html";
665 * Processes and stores configuration data for this authentication plugin.
667 function process_config($config) {
668 // set to defaults if undefined
669 if (!isset ($config->rpc_negotiation_timeout)) {
670 $config->rpc_negotiation_timeout = '30';
673 if (!isset ($config->auto_add_remote_users)) {
674 $config->auto_add_remote_users = '0';
675 } See MDL-21327 for why this is commented out
676 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth_mnet');
679 // save settings
680 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
682 return true;
686 * Poll the IdP server to let it know that a user it has authenticated is still
687 * online
689 * @return void
691 function keepalive_client() {
692 global $CFG, $DB;
693 $cutoff = time() - 300; // TODO - find out what the remote server's session
694 // cutoff is, and preempt that
696 $sql = "
697 select
699 username,
700 mnethostid
701 from
702 {user}
703 where
704 lastaccess > ? AND
705 mnethostid != ?
706 order by
707 mnethostid";
709 $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
711 if ($immigrants == false) {
712 return true;
715 $usersArray = array();
716 foreach($immigrants as $immigrant) {
717 $usersArray[$immigrant->mnethostid][] = $immigrant->username;
720 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
721 foreach($usersArray as $mnethostid => $users) {
722 $mnet_peer = new mnet_peer();
723 $mnet_peer->set_id($mnethostid);
725 $mnet_request = new mnet_xmlrpc_client();
726 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
728 // set $token and $useragent parameters
729 $mnet_request->add_param($users);
731 if ($mnet_request->send($mnet_peer) === true) {
732 if (!isset($mnet_request->response['code'])) {
733 debugging("Server side error has occured on host $mnethostid");
734 continue;
735 } elseif ($mnet_request->response['code'] > 0) {
736 debugging($mnet_request->response['message']);
739 if (!isset($mnet_request->response['last log id'])) {
740 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
741 continue;
743 } else {
744 debugging("Server side error has occured on host $mnethostid: " .
745 join("\n", $mnet_request->error));
746 break;
748 $mnethostlogssql = "
749 SELECT
750 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
751 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
752 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
753 c.modinfo
754 FROM
756 SELECT
757 l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
758 l.action, l.url, l.info, u.username
759 FROM
760 {user} u
761 INNER JOIN {log} l on l.userid = u.id
762 WHERE
763 u.mnethostid = ?
764 AND l.id > ?
765 ORDER BY remoteid ASC
766 LIMIT 500
767 ) mhostlogs
768 INNER JOIN {course} c on c.id = mhostlogs.course
769 ORDER by mhostlogs.remoteid ASC";
771 $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
773 if ($mnethostlogs == false) {
774 continue;
777 $processedlogs = array();
779 foreach($mnethostlogs as $hostlog) {
780 // Extract the name of the relevant module instance from the
781 // course modinfo if possible.
782 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
783 $modinfo = unserialize($hostlog->modinfo);
784 unset($hostlog->modinfo);
785 $modulearray = array();
786 foreach($modinfo as $module) {
787 $modulearray[$module->cm] = $module->name;
789 $hostlog->resource_name = $modulearray[$hostlog->cmid];
790 } else {
791 $hostlog->resource_name = '';
794 $processedlogs[] = array (
795 'remoteid' => $hostlog->remoteid,
796 'time' => $hostlog->time,
797 'userid' => $hostlog->userid,
798 'ip' => $hostlog->ip,
799 'course' => $hostlog->course,
800 'coursename' => $hostlog->coursename,
801 'module' => $hostlog->module,
802 'cmid' => $hostlog->cmid,
803 'action' => $hostlog->action,
804 'url' => $hostlog->url,
805 'info' => $hostlog->info,
806 'resource_name' => $hostlog->resource_name,
807 'username' => $hostlog->username
811 unset($hostlog);
813 $mnet_request = new mnet_xmlrpc_client();
814 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
816 // set $token and $useragent parameters
817 $mnet_request->add_param($processedlogs);
819 if ($mnet_request->send($mnet_peer) === true) {
820 if ($mnet_request->response['code'] > 0) {
821 debugging($mnet_request->response['message']);
823 } else {
824 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
830 * Receives an array of log entries from an SP and adds them to the mnet_log
831 * table
833 * @param array $array An array of usernames
834 * @return string "All ok" or an error message
836 function refresh_log($array) {
837 global $CFG, $DB;
838 $remoteclient = get_mnet_remote_client();
840 // We don't want to output anything to the client machine
841 $start = ob_start();
843 $returnString = '';
844 $transaction = $DB->start_delegated_transaction();
845 $useridarray = array();
847 foreach($array as $logEntry) {
848 $logEntryObj = (object)$logEntry;
849 $logEntryObj->hostid = $remoteclient->id;
851 if (isset($useridarray[$logEntryObj->username])) {
852 $logEntryObj->userid = $useridarray[$logEntryObj->username];
853 } else {
854 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
855 if ($logEntryObj->userid == false) {
856 $logEntryObj->userid = 0;
858 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
861 unset($logEntryObj->username);
863 $logEntryObj = $this->trim_logline($logEntryObj);
864 $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
866 if ($insertok) {
867 $remoteclient->last_log_id = $logEntryObj->remoteid;
868 } else {
869 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
872 $remoteclient->commit();
873 $transaction->allow_commit();
875 $end = ob_end_clean();
877 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
878 return array('code' => 1, 'message' => $returnString);
882 * Receives an array of usernames from a remote machine and prods their
883 * sessions to keep them alive
885 * @param array $array An array of usernames
886 * @return string "All ok" or an error message
888 function keepalive_server($array) {
889 global $CFG, $DB;
890 $remoteclient = get_mnet_remote_client();
892 $CFG->usesid = true;
894 // We don't want to output anything to the client machine
895 $start = ob_start();
897 // We'll get session records in batches of 30
898 $superArray = array_chunk($array, 30);
900 $returnString = '';
902 foreach($superArray as $subArray) {
903 $subArray = array_values($subArray);
904 $instring = "('".implode("', '",$subArray)."')";
905 $query = "select id, session_id, username from {mnet_session} where username in $instring";
906 $results = $DB->get_records_sql($query);
908 if ($results == false) {
909 // We seem to have a username that breaks our query:
910 // TODO: Handle this error appropriately
911 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
912 } else {
913 foreach($results as $emigrant) {
914 session_touch($emigrant->session_id);
919 $end = ob_end_clean();
921 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
922 return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
926 * Cron function will be called automatically by cron.php every 5 minutes
928 * @return void
930 function cron() {
931 global $DB;
933 // run the keepalive client
934 $this->keepalive_client();
936 // admin/cron.php should have run srand for us
937 $random100 = rand(0,100);
938 if ($random100 < 10) { // Approximately 10% of the time.
939 // nuke olden sessions
940 $longtime = time() - (1 * 3600 * 24);
941 $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
946 * Cleanup any remote mnet_sessions, kill the local mnet_session data
948 * This is called by require_logout in moodlelib
950 * @return void
952 function prelogout_hook() {
953 global $CFG, $USER;
955 if (!is_enabled_auth('mnet')) {
956 return;
959 // If the user is local to this Moodle:
960 if ($USER->mnethostid == $this->mnet->id) {
961 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
963 // Else the user has hit 'logout' at a Service Provider Moodle:
964 } else {
965 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
971 * The SP uses this function to kill the session on the parent IdP
973 * @param string $username Username for session to kill
974 * @param string $useragent SHA1 hash of user agent to look for
975 * @return string A plaintext report of what has happened
977 function kill_parent($username, $useragent) {
978 global $CFG, $USER, $DB;
980 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
981 $sql = "
982 select
984 from
985 {mnet_session} s
986 where
987 s.username = ? AND
988 s.useragent = ? AND
989 s.mnethostid = ?";
991 $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
993 $ignore = $DB->delete_records('mnet_session',
994 array('username'=>$username,
995 'useragent'=>$useragent,
996 'mnethostid'=>$USER->mnethostid));
998 if (false != $mnetsessions) {
999 $mnet_peer = new mnet_peer();
1000 $mnet_peer->set_id($USER->mnethostid);
1002 $mnet_request = new mnet_xmlrpc_client();
1003 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1005 // set $token and $useragent parameters
1006 $mnet_request->add_param($username);
1007 $mnet_request->add_param($useragent);
1008 if ($mnet_request->send($mnet_peer) === false) {
1009 debugging(join("\n", $mnet_request->error));
1010 return false;
1014 return true;
1018 * The IdP uses this function to kill child sessions on other hosts
1020 * @param string $username Username for session to kill
1021 * @param string $useragent SHA1 hash of user agent to look for
1022 * @return string A plaintext report of what has happened
1024 function kill_children($username, $useragent) {
1025 global $CFG, $USER, $DB;
1026 $remoteclient = null;
1027 if (defined('MNET_SERVER')) {
1028 $remoteclient = get_mnet_remote_client();
1030 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1032 $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1034 $returnstring = '';
1036 $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1038 if (false == $mnetsessions) {
1039 $returnstring .= "Could find no remote sessions\n";
1040 $mnetsessions = array();
1043 foreach($mnetsessions as $mnetsession) {
1044 // If this script is being executed by a remote peer, that means the user has clicked
1045 // logout on that peer, and the session on that peer can be deleted natively.
1046 // Skip over it.
1047 if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1048 continue;
1050 $returnstring .= "Deleting session\n";
1052 $mnet_peer = new mnet_peer();
1053 $mnet_peer->set_id($mnetsession->mnethostid);
1055 $mnet_request = new mnet_xmlrpc_client();
1056 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1058 // set $token and $useragent parameters
1059 $mnet_request->add_param($username);
1060 $mnet_request->add_param($useragent);
1061 if ($mnet_request->send($mnet_peer) === false) {
1062 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1063 join("\n", $mnet_request->error));
1067 $ignore = $DB->delete_records('mnet_session',
1068 array('useragent'=>$useragent, 'userid'=>$userid));
1070 if (isset($remoteclient) && isset($remoteclient->id)) {
1071 session_kill_user($userid);
1073 return $returnstring;
1077 * When the IdP requests that child sessions are terminated,
1078 * this function will be called on each of the child hosts. The machine that
1079 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1081 * @param string $username Username for session to kill
1082 * @param string $useragent SHA1 hash of user agent to look for
1083 * @return bool True on success
1085 function kill_child($username, $useragent) {
1086 global $CFG, $DB;
1087 $remoteclient = get_mnet_remote_client();
1088 $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1089 $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1090 if (false != $session) {
1091 session_kill($session->session_id);
1092 return true;
1094 return false;
1098 * To delete a host, we must delete all current sessions that users from
1099 * that host are currently engaged in.
1101 * @param string $sessionidarray An array of session hashes
1102 * @return bool True on success
1104 function end_local_sessions(&$sessionArray) {
1105 global $CFG;
1106 if (is_array($sessionArray)) {
1107 while($session = array_pop($sessionArray)) {
1108 session_kill($session->session_id);
1110 return true;
1112 return false;
1116 * Returns the user's profile image info
1118 * If the user exists and has a profile picture, the returned array will contain keys:
1119 * f1 - the content of the default 100x100px image
1120 * f1_mimetype - the mimetype of the f1 file
1121 * f2 - the content of the 35x35px variant of the image
1122 * f2_mimetype - the mimetype of the f2 file
1124 * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1126 * @see process_new_icon()
1127 * @uses mnet_remote_client callable via MNet XML-RPC
1128 * @param int $userid The id of the user
1129 * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1131 function fetch_user_image($username) {
1132 global $CFG, $DB;
1134 if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
1135 $fs = get_file_storage();
1136 $usercontext = get_context_instance(CONTEXT_USER, $user->id, MUST_EXIST);
1137 $return = array();
1138 if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
1139 $return['f1'] = base64_encode($f1->get_content());
1140 $return['f1_mimetype'] = $f1->get_mimetype();
1141 } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
1142 $return['f1'] = base64_encode($f1->get_content());
1143 $return['f1_mimetype'] = $f1->get_mimetype();
1145 if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
1146 $return['f2'] = base64_encode($f2->get_content());
1147 $return['f2_mimetype'] = $f2->get_mimetype();
1148 } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
1149 $return['f2'] = base64_encode($f2->get_content());
1150 $return['f2_mimetype'] = $f2->get_mimetype();
1152 return $return;
1154 return false;
1158 * Returns the theme information and logo url as strings.
1160 * @return string The theme info
1162 function fetch_theme_info() {
1163 global $CFG;
1165 $themename = "$CFG->theme";
1166 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1168 $return['themename'] = $themename;
1169 $return['logourl'] = $logourl;
1170 return $return;
1174 * Determines if an MNET host is providing the nominated service.
1176 * @param int $mnethostid The id of the remote host
1177 * @param string $servicename The name of the service
1178 * @return bool Whether the service is available on the remote host
1180 function has_service($mnethostid, $servicename) {
1181 global $CFG, $DB;
1183 $sql = "
1184 SELECT
1185 svc.id as serviceid,
1186 svc.name,
1187 svc.description,
1188 svc.offer,
1189 svc.apiversion,
1190 h2s.id as h2s_id
1191 FROM
1192 {mnet_host} h,
1193 {mnet_service} svc,
1194 {mnet_host2service} h2s
1195 WHERE
1196 h.deleted = '0' AND
1197 h.id = h2s.hostid AND
1198 h2s.hostid = ? AND
1199 h2s.serviceid = svc.id AND
1200 svc.name = ? AND
1201 h2s.subscribe = '1'";
1203 return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1207 * Checks the MNET access control table to see if the username/mnethost
1208 * is permitted to login to this moodle.
1210 * @param string $username The username
1211 * @param int $mnethostid The id of the remote mnethost
1212 * @return bool Whether the user can login from the remote host
1214 function can_login_remotely($username, $mnethostid) {
1215 global $DB;
1217 $accessctrl = 'allow';
1218 $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1219 if (!empty($aclrecord)) {
1220 $accessctrl = $aclrecord->accessctrl;
1222 return $accessctrl == 'allow';
1225 function logoutpage_hook() {
1226 global $USER, $CFG, $redirect, $DB;
1228 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1229 $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1230 $redirect = $host->wwwroot.'/';
1235 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1237 * @param object $logline The log information to be trimmed
1238 * @return object The passed logline object trimmed to not exceed storable limits
1240 function trim_logline ($logline) {
1241 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1242 'url' => 255);
1243 foreach ($limits as $property => $limit) {
1244 if (isset($logline->$property)) {
1245 $logline->$property = substr($logline->$property, 0, $limit);
1249 return $logline;
1253 * Returns a list of potential IdPs that this authentication plugin supports.
1254 * This is used to provide links on the login page.
1256 * @param string $wantsurl the relative url fragment the user wants to get to. You can use this to compose a returnurl, for example
1258 * @return array like:
1259 * array(
1260 * array(
1261 * 'url' => 'http://someurl',
1262 * 'icon' => new pix_icon(...),
1263 * 'name' => get_string('somename', 'auth_yourplugin'),
1264 * ),
1267 function loginpage_idp_list($wantsurl) {
1268 global $DB, $CFG;
1270 // strip off wwwroot, since the remote site will prefix it's return url with this
1271 $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1273 $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1274 FROM {mnet_host} h
1275 JOIN {mnet_host2service} m ON h.id = m.hostid
1276 JOIN {mnet_service} s ON s.id = m.serviceid
1277 JOIN {mnet_application} a ON h.applicationid = a.id
1278 WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1279 $params = array('sso_sp', 0, 1);
1281 if (!empty($CFG->mnet_all_hosts_id)) {
1282 $sql .= " AND h.id <> ?";
1283 $params[] = $CFG->mnet_all_hosts_id;
1286 if (!$hosts = $DB->get_records_sql($sql, $params)) {
1287 return array();
1290 $idps = array();
1291 foreach ($hosts as $host) {
1292 $idps[] = array(
1293 'url' => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1294 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1295 'name' => $host->name,
1298 return $idps;