MDL-45536 atto_html: Update the textarea size when switching to HTML view
[moodle.git] / auth / mnet / auth.php
blobaa8e1cead427833faebd3f9a42e92408ee8c9167
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Authentication Plugin: Moodle Network Authentication
19 * Multiple host authentication support for Moodle Network.
21 * @package auth_mnet
22 * @author Martin Dougiamas
23 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->libdir.'/authlib.php');
30 /**
31 * Moodle Network authentication plugin.
33 class auth_plugin_mnet extends auth_plugin_base {
35 /**
36 * Constructor.
38 function auth_plugin_mnet() {
39 $this->authtype = 'mnet';
40 $this->config = get_config('auth_mnet');
41 $this->mnet = get_mnet_environment();
44 /**
45 * This function is normally used to determine if the username and password
46 * are correct for local logins. Always returns false, as local users do not
47 * need to login over mnet xmlrpc.
49 * @param string $username The username
50 * @param string $password The password
51 * @return bool Authentication success or failure.
53 function user_login($username, $password) {
54 return false; // print_error("mnetlocal");
57 /**
58 * Return user data for the provided token, compare with user_agent string.
60 * @param string $token The unique ID provided by remotehost.
61 * @param string $UA User Agent string.
62 * @return array $userdata Array of user info for remote host
64 function user_authorise($token, $useragent) {
65 global $CFG, $SITE, $DB;
66 $remoteclient = get_mnet_remote_client();
67 require_once $CFG->dirroot . '/mnet/xmlrpc/serverlib.php';
69 $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent));
70 if (empty($mnet_session)) {
71 throw new mnet_server_exception(1, 'authfail_nosessionexists');
74 // check session confirm timeout
75 if ($mnet_session->confirm_timeout < time()) {
76 throw new mnet_server_exception(2, 'authfail_sessiontimedout');
79 // session okay, try getting the user
80 if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid))) {
81 throw new mnet_server_exception(3, 'authfail_usermismatch');
84 $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient));
86 // extra special ones
87 $userdata['auth'] = 'mnet';
88 $userdata['wwwroot'] = $this->mnet->wwwroot;
89 $userdata['session.gc_maxlifetime'] = ini_get('session.gc_maxlifetime');
91 if (array_key_exists('picture', $userdata) && !empty($user->picture)) {
92 $fs = get_file_storage();
93 $usercontext = context_user::instance($user->id, MUST_EXIST);
94 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
95 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
96 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
97 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
98 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
99 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
103 $userdata['myhosts'] = array();
104 if ($courses = enrol_get_users_courses($user->id, false)) {
105 $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
108 $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
109 COUNT(c.id) AS count
110 FROM {mnetservice_enrol_courses} c
111 JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
112 JOIN {mnet_host} h ON h.id = c.hostid
113 WHERE e.userid = ? AND c.hostid = ?
114 GROUP BY h.name, h.wwwroot, h.id";
116 if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) {
117 foreach($courses as $course) {
118 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
122 return $userdata;
126 * Generate a random string for use as an RPC session token.
128 function generate_token() {
129 return sha1(str_shuffle('' . mt_rand() . time()));
133 * Starts an RPC jump session and returns the jump redirect URL.
135 * @param int $mnethostid id of the mnet host to jump to
136 * @param string $wantsurl url to redirect to after the jump (usually on remote system)
137 * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
138 * rather than somewhere inside *its* wwwroot
140 function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
141 global $CFG, $USER, $DB;
142 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
144 if (\core\session\manager::is_loggedinas()) {
145 print_error('notpermittedtojumpas', 'mnet');
148 // check remote login permissions
149 if (! has_capability('moodle/site:mnetlogintoremote', context_system::instance())
150 or is_mnet_remote_user($USER)
151 or isguestuser()
152 or !isloggedin()) {
153 print_error('notpermittedtojump', 'mnet');
156 // check for SSO publish permission first
157 if ($this->has_service($mnethostid, 'sso_sp') == false) {
158 print_error('hostnotconfiguredforsso', 'mnet');
161 // set RPC timeout to 30 seconds if not configured
162 if (empty($this->config->rpc_negotiation_timeout)) {
163 $this->config->rpc_negotiation_timeout = 30;
164 set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
167 // get the host info
168 $mnet_peer = new mnet_peer();
169 $mnet_peer->set_id($mnethostid);
171 // set up the session
172 $mnet_session = $DB->get_record('mnet_session',
173 array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
174 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
175 if ($mnet_session == false) {
176 $mnet_session = new stdClass();
177 $mnet_session->mnethostid = $mnethostid;
178 $mnet_session->userid = $USER->id;
179 $mnet_session->username = $USER->username;
180 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
181 $mnet_session->token = $this->generate_token();
182 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
183 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
184 $mnet_session->session_id = session_id();
185 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
186 } else {
187 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
188 $mnet_session->token = $this->generate_token();
189 $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
190 $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
191 $mnet_session->session_id = session_id();
192 $DB->update_record('mnet_session', $mnet_session);
195 // construct the redirection URL
196 //$transport = mnet_get_protocol($mnet_peer->transport);
197 $wantsurl = urlencode($wantsurl);
198 $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
199 if ($wantsurlbackhere) {
200 $url .= '&remoteurl=1';
203 return $url;
207 * This function confirms the remote (ID provider) host's mnet session
208 * by communicating the token and UA over the XMLRPC transport layer, and
209 * returns the local user record on success.
211 * @param string $token The random session token.
212 * @param mnet_peer $remotepeer The ID provider mnet_peer object.
213 * @return array The local user record.
215 function confirm_mnet_session($token, $remotepeer) {
216 global $CFG, $DB;
217 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
218 require_once $CFG->libdir . '/gdlib.php';
219 require_once($CFG->dirroot.'/user/lib.php');
221 // verify the remote host is configured locally before attempting RPC call
222 if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
223 print_error('notpermittedtoland', 'mnet');
226 // set up the RPC request
227 $mnetrequest = new mnet_xmlrpc_client();
228 $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
230 // set $token and $useragent parameters
231 $mnetrequest->add_param($token);
232 $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
234 // Thunderbirds are go! Do RPC call and store response
235 if ($mnetrequest->send($remotepeer) === true) {
236 $remoteuser = (object) $mnetrequest->response;
237 } else {
238 foreach ($mnetrequest->error as $errormessage) {
239 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
240 if($code == 702) {
241 $site = get_site();
242 print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname));
243 exit;
245 $message .= "ERROR $code:<br/>$errormessage<br/>";
247 print_error("rpcerror", '', '', $message);
249 unset($mnetrequest);
251 if (empty($remoteuser) or empty($remoteuser->username)) {
252 print_error('unknownerror', 'mnet');
253 exit;
256 if (user_not_fully_set_up($remoteuser)) {
257 print_error('notenoughidpinfo', 'mnet');
258 exit;
261 $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
263 $remoteuser->auth = 'mnet';
264 $remoteuser->wwwroot = $remotepeer->wwwroot;
266 // the user may roam from Moodle 1.x where lang has _utf8 suffix
267 // also, make sure that the lang is actually installed, otherwise set site default
268 if (isset($remoteuser->lang)) {
269 $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
271 if (empty($remoteuser->lang)) {
272 if (!empty($CFG->lang)) {
273 $remoteuser->lang = $CFG->lang;
274 } else {
275 $remoteuser->lang = 'en';
278 $firsttime = false;
280 // get the local record for the remote user
281 $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
283 // add the remote user to the database if necessary, and if allowed
284 // TODO: refactor into a separate function
285 if (empty($localuser) || ! $localuser->id) {
287 if (empty($this->config->auto_add_remote_users)) {
288 print_error('nolocaluser', 'mnet');
289 } See MDL-21327 for why this is commented out
291 $remoteuser->mnethostid = $remotehost->id;
292 $remoteuser->firstaccess = time(); // First time user in this server, grab it here
293 $remoteuser->confirmed = 1;
295 $remoteuser->id = $DB->insert_record('user', $remoteuser);
296 $firsttime = true;
297 $localuser = $remoteuser;
300 // check sso access control list for permission first
301 if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
302 print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
305 $fs = get_file_storage();
307 // update the local user record with remote user data
308 foreach ((array) $remoteuser as $key => $val) {
310 if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
311 // update the user picture if there is a newer verion at the identity provider
312 $usercontext = context_user::instance($localuser->id, MUST_EXIST);
313 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
314 $localtimemodified = $usericonfile->get_timemodified();
315 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
316 $localtimemodified = $usericonfile->get_timemodified();
317 } else {
318 $localtimemodified = 0;
321 if (!empty($val) and $localtimemodified < $val) {
322 mnet_debug('refetching the user picture from the identity provider host');
323 $fetchrequest = new mnet_xmlrpc_client();
324 $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
325 $fetchrequest->add_param($localuser->username);
326 if ($fetchrequest->send($remotepeer) === true) {
327 if (strlen($fetchrequest->response['f1']) > 0) {
328 $imagefilename = $CFG->tempdir . '/mnet-usericon-' . $localuser->id;
329 $imagecontents = base64_decode($fetchrequest->response['f1']);
330 file_put_contents($imagefilename, $imagecontents);
331 if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
332 $localuser->picture = $newrev;
334 unlink($imagefilename);
336 // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
337 // the mimetype information provided is ignored and the type of the file is detected
338 // by process_new_icon()
343 if($key == 'myhosts') {
344 $localuser->mnet_foreign_host_array = array();
345 foreach($val as $rhost) {
346 $name = clean_param($rhost['name'], PARAM_ALPHANUM);
347 $url = clean_param($rhost['url'], PARAM_URL);
348 $count = clean_param($rhost['count'], PARAM_INT);
349 $url_is_local = stristr($url , $CFG->wwwroot);
350 if (!empty($name) && !empty($count) && empty($url_is_local)) {
351 $localuser->mnet_foreign_host_array[] = array('name' => $name,
352 'url' => $url,
353 'count' => $count);
358 $localuser->{$key} = $val;
361 $localuser->mnethostid = $remotepeer->id;
362 if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
363 $localuser->firstaccess = time();
365 user_update_user($localuser, false);
367 if (!$firsttime) {
368 // repeat customer! let the IDP know about enrolments
369 // we have for this user.
370 // set up the RPC request
371 $mnetrequest = new mnet_xmlrpc_client();
372 $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
374 // pass username and an assoc array of "my courses"
375 // with info so that the IDP can maintain mnetservice_enrol_enrolments
376 $mnetrequest->add_param($remoteuser->username);
377 $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
378 $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
379 if (is_array($courses) && !empty($courses)) {
380 // Second request to do the JOINs that we'd have done
381 // inside enrol_get_users_courses() if we had been allowed
382 $sql = "SELECT c.id,
383 cc.name AS cat_name, cc.description AS cat_description
384 FROM {course} c
385 JOIN {course_categories} cc ON c.category = cc.id
386 WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
387 $extra = $DB->get_records_sql($sql);
389 $keys = array_keys($courses);
390 $studentroles = get_archetype_roles('student');
391 if (!empty($studentroles)) {
392 $defaultrole = reset($studentroles);
393 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
394 foreach ($keys AS $id) {
395 if ($courses[$id]->visible == 0) {
396 unset($courses[$id]);
397 continue;
399 $courses[$id]->cat_id = $courses[$id]->category;
400 $courses[$id]->defaultroleid = $defaultrole->id;
401 unset($courses[$id]->category);
402 unset($courses[$id]->visible);
404 $courses[$id]->cat_name = $extra[$id]->cat_name;
405 $courses[$id]->cat_description = $extra[$id]->cat_description;
406 $courses[$id]->defaultrolename = $defaultrole->name;
407 // coerce to array
408 $courses[$id] = (array)$courses[$id];
410 } else {
411 throw new moodle_exception('unknownrole', 'error', '', 'student');
413 } else {
414 // if the array is empty, send it anyway
415 // we may be clearing out stale entries
416 $courses = array();
418 $mnetrequest->add_param($courses);
420 // Call 0800-RPC Now! -- we don't care too much if it fails
421 // as it's just informational.
422 if ($mnetrequest->send($remotepeer) === false) {
423 // error_log(print_r($mnetrequest->error,1));
427 return $localuser;
432 * creates (or updates) the mnet session once
433 * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
435 * @param stdclass $user the local user (must exist already
436 * @param string $token the jump/land token
437 * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
439 public function update_mnet_session($user, $token, $remotepeer) {
440 global $DB;
441 $session_gc_maxlifetime = 1440;
442 if (isset($user->session_gc_maxlifetime)) {
443 $session_gc_maxlifetime = $user->session_gc_maxlifetime;
445 if (!$mnet_session = $DB->get_record('mnet_session',
446 array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
447 'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
448 $mnet_session = new stdClass();
449 $mnet_session->mnethostid = $remotepeer->id;
450 $mnet_session->userid = $user->id;
451 $mnet_session->username = $user->username;
452 $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
453 $mnet_session->token = $token; // Needed to support simultaneous sessions
454 // and preserving DB rec uniqueness
455 $mnet_session->confirm_timeout = time();
456 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
457 $mnet_session->session_id = session_id();
458 $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
459 } else {
460 $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
461 $DB->update_record('mnet_session', $mnet_session);
468 * Invoke this function _on_ the IDP to update it with enrolment info local to
469 * the SP right after calling user_authorise()
471 * Normally called by the SP after calling user_authorise()
473 * @param string $username The username
474 * @param array $courses Assoc array of courses following the structure of mnetservice_enrol_courses
475 * @return bool
477 function update_enrolments($username, $courses) {
478 global $CFG, $DB;
479 $remoteclient = get_mnet_remote_client();
481 if (empty($username) || !is_array($courses)) {
482 return false;
484 // make sure it is a user we have an in active session
485 // with that host...
486 $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
487 $userid = null;
488 foreach ($mnetsessions as $mnetsession) {
489 if (is_null($userid)) {
490 $userid = $mnetsession->userid;
491 continue;
493 if ($userid != $mnetsession->userid) {
494 throw new mnet_server_exception(3, 'authfail_usermismatch');
498 if (empty($courses)) { // no courses? clear out quickly
499 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
500 return true;
503 // IMPORTANT: Ask for remoteid as the first element in the query, so
504 // that the array that comes back is indexed on the same field as the
505 // array that we have received from the remote client
506 $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
507 c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
508 e.id AS enrolmentid
509 FROM {mnetservice_enrol_courses} c
510 LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
511 WHERE e.userid = ? AND c.hostid = ?";
513 $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
515 $local_courseid_array = array();
516 foreach($courses as $ix => $course) {
518 $course['remoteid'] = $course['id'];
519 $course['hostid'] = (int)$remoteclient->id;
520 $userisregd = false;
522 // if we do not have the the information about the remote course, it is not available
523 // to us for remote enrolment - skip
524 if (array_key_exists($course['remoteid'], $currentcourses)) {
525 // Pointer to current course:
526 $currentcourse =& $currentcourses[$course['remoteid']];
527 // We have a record - is it up-to-date?
528 $course['id'] = $currentcourse->id;
530 $saveflag = false;
532 foreach($course as $key => $value) {
533 if ($currentcourse->$key != $value) {
534 $saveflag = true;
535 $currentcourse->$key = $value;
539 if ($saveflag) {
540 $DB->update_record('mnetervice_enrol_courses', $currentcourse);
543 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
544 $userisregd = true;
546 } else {
547 unset ($courses[$ix]);
548 continue;
551 // By this point, we should always have a $dataObj->id
552 $local_courseid_array[] = $course['id'];
554 // Do we have a record for this assignment?
555 if ($userisregd) {
556 // Yes - we know about this one already
557 // We don't want to do updates because the new data is probably
558 // 'less complete' than the data we have.
559 } else {
560 // No - create a record
561 $assignObj = new stdClass();
562 $assignObj->userid = $userid;
563 $assignObj->hostid = (int)$remoteclient->id;
564 $assignObj->remotecourseid = $course['remoteid'];
565 $assignObj->rolename = $course['defaultrolename'];
566 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
570 // Clean up courses that the user is no longer enrolled in.
571 if (!empty($local_courseid_array)) {
572 $local_courseid_string = implode(', ', $local_courseid_array);
573 $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
574 $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
578 function prevent_local_passwords() {
579 return true;
583 * Returns true if this authentication plugin is 'internal'.
585 * @return bool
587 function is_internal() {
588 return false;
592 * Returns true if this authentication plugin can change the user's
593 * password.
595 * @return bool
597 function can_change_password() {
598 //TODO: it should be able to redirect, right?
599 return false;
603 * Returns the URL for changing the user's pw, or false if the default can
604 * be used.
606 * @return moodle_url
608 function change_password_url() {
609 return null;
613 * Prints a form for configuring this authentication plugin.
615 * This function is called from admin/auth.php, and outputs a full page with
616 * a form for configuring this plugin.
618 * @param object $config
619 * @param object $err
620 * @param array $user_fields
622 function config_form($config, $err, $user_fields) {
623 global $CFG, $DB;
625 $query = "
626 SELECT
627 h.id,
628 h.name as hostname,
629 h.wwwroot,
630 h2idp.publish as idppublish,
631 h2idp.subscribe as idpsubscribe,
632 idp.name as idpname,
633 h2sp.publish as sppublish,
634 h2sp.subscribe as spsubscribe,
635 sp.name as spname
636 FROM
637 {mnet_host} h
638 LEFT JOIN
639 {mnet_host2service} h2idp
641 (h.id = h2idp.hostid AND
642 (h2idp.publish = 1 OR
643 h2idp.subscribe = 1))
644 INNER JOIN
645 {mnet_service} idp
647 (h2idp.serviceid = idp.id AND
648 idp.name = 'sso_idp')
649 LEFT JOIN
650 {mnet_host2service} h2sp
652 (h.id = h2sp.hostid AND
653 (h2sp.publish = 1 OR
654 h2sp.subscribe = 1))
655 INNER JOIN
656 {mnet_service} sp
658 (h2sp.serviceid = sp.id AND
659 sp.name = 'sso_sp')
660 WHERE
661 ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
662 (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
663 h.id != ?
664 ORDER BY
665 h.name ASC";
667 $id_providers = array();
668 $service_providers = array();
669 if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
670 foreach($resultset as $hostservice) {
671 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
672 $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
674 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
675 $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
680 include "config.html";
684 * Processes and stores configuration data for this authentication plugin.
686 function process_config($config) {
687 // set to defaults if undefined
688 if (!isset ($config->rpc_negotiation_timeout)) {
689 $config->rpc_negotiation_timeout = '30';
692 if (!isset ($config->auto_add_remote_users)) {
693 $config->auto_add_remote_users = '0';
694 } See MDL-21327 for why this is commented out
695 set_config('auto_add_remote_users', $config->auto_add_remote_users, 'auth_mnet');
698 // save settings
699 set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
701 return true;
705 * Poll the IdP server to let it know that a user it has authenticated is still
706 * online
708 * @return void
710 function keepalive_client() {
711 global $CFG, $DB;
712 $cutoff = time() - 300; // TODO - find out what the remote server's session
713 // cutoff is, and preempt that
715 $sql = "
716 select
718 username,
719 mnethostid
720 from
721 {user}
722 where
723 lastaccess > ? AND
724 mnethostid != ?
725 order by
726 mnethostid";
728 $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
730 if ($immigrants == false) {
731 return true;
734 $usersArray = array();
735 foreach($immigrants as $immigrant) {
736 $usersArray[$immigrant->mnethostid][] = $immigrant->username;
739 require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
740 foreach($usersArray as $mnethostid => $users) {
741 $mnet_peer = new mnet_peer();
742 $mnet_peer->set_id($mnethostid);
744 $mnet_request = new mnet_xmlrpc_client();
745 $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
747 // set $token and $useragent parameters
748 $mnet_request->add_param($users);
750 if ($mnet_request->send($mnet_peer) === true) {
751 if (!isset($mnet_request->response['code'])) {
752 debugging("Server side error has occured on host $mnethostid");
753 continue;
754 } elseif ($mnet_request->response['code'] > 0) {
755 debugging($mnet_request->response['message']);
758 if (!isset($mnet_request->response['last log id'])) {
759 debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
760 continue;
762 } else {
763 debugging("Server side error has occured on host $mnethostid: " .
764 join("\n", $mnet_request->error));
765 break;
767 $mnethostlogssql = "
768 SELECT
769 l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
770 l.action, l.url, l.info, u.username
771 FROM
772 {user} u
773 INNER JOIN {log} l on l.userid = u.id
774 WHERE
775 u.mnethostid = ?
776 AND l.id > ?
777 AND l.course IS NOT NULL
778 ORDER by l.id ASC";
780 $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']), 0, 500);
782 if ($mnethostlogs == false) {
783 continue;
786 $processedlogs = array();
788 foreach($mnethostlogs as $hostlog) {
789 try {
790 // Get impersonalised course information. If it is cached there will be no DB queries.
791 $modinfo = get_fast_modinfo($hostlog->course, -1);
792 $hostlog->coursename = $modinfo->get_course()->fullname;
793 if (!empty($hostlog->cmid) && isset($modinfo->cms[$hostlog->cmid])) {
794 $hostlog->resource_name = $modinfo->cms[$hostlog->cmid]->name;
795 } else {
796 $hostlog->resource_name = '';
798 } catch (moodle_exception $e) {
799 // Course not found
800 continue;
803 $processedlogs[] = array (
804 'remoteid' => $hostlog->remoteid,
805 'time' => $hostlog->time,
806 'userid' => $hostlog->userid,
807 'ip' => $hostlog->ip,
808 'course' => $hostlog->course,
809 'coursename' => $hostlog->coursename,
810 'module' => $hostlog->module,
811 'cmid' => $hostlog->cmid,
812 'action' => $hostlog->action,
813 'url' => $hostlog->url,
814 'info' => $hostlog->info,
815 'resource_name' => $hostlog->resource_name,
816 'username' => $hostlog->username
820 unset($hostlog);
822 $mnet_request = new mnet_xmlrpc_client();
823 $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
825 // set $token and $useragent parameters
826 $mnet_request->add_param($processedlogs);
828 if ($mnet_request->send($mnet_peer) === true) {
829 if ($mnet_request->response['code'] > 0) {
830 debugging($mnet_request->response['message']);
832 } else {
833 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
839 * Receives an array of log entries from an SP and adds them to the mnet_log
840 * table
842 * @param array $array An array of usernames
843 * @return string "All ok" or an error message
845 function refresh_log($array) {
846 global $CFG, $DB;
847 $remoteclient = get_mnet_remote_client();
849 // We don't want to output anything to the client machine
850 $start = ob_start();
852 $returnString = '';
853 $transaction = $DB->start_delegated_transaction();
854 $useridarray = array();
856 foreach($array as $logEntry) {
857 $logEntryObj = (object)$logEntry;
858 $logEntryObj->hostid = $remoteclient->id;
860 if (isset($useridarray[$logEntryObj->username])) {
861 $logEntryObj->userid = $useridarray[$logEntryObj->username];
862 } else {
863 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
864 if ($logEntryObj->userid == false) {
865 $logEntryObj->userid = 0;
867 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
870 unset($logEntryObj->username);
872 $logEntryObj = $this->trim_logline($logEntryObj);
873 $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
875 if ($insertok) {
876 $remoteclient->last_log_id = $logEntryObj->remoteid;
877 } else {
878 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
881 $remoteclient->commit();
882 $transaction->allow_commit();
884 $end = ob_end_clean();
886 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
887 return array('code' => 1, 'message' => $returnString);
891 * Receives an array of usernames from a remote machine and prods their
892 * sessions to keep them alive
894 * @param array $array An array of usernames
895 * @return string "All ok" or an error message
897 function keepalive_server($array) {
898 global $CFG, $DB;
899 $remoteclient = get_mnet_remote_client();
901 // We don't want to output anything to the client machine
902 $start = ob_start();
904 // We'll get session records in batches of 30
905 $superArray = array_chunk($array, 30);
907 $returnString = '';
909 foreach($superArray as $subArray) {
910 $subArray = array_values($subArray);
911 $instring = "('".implode("', '",$subArray)."')";
912 $query = "select id, session_id, username from {mnet_session} where username in $instring";
913 $results = $DB->get_records_sql($query);
915 if ($results == false) {
916 // We seem to have a username that breaks our query:
917 // TODO: Handle this error appropriately
918 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
919 } else {
920 foreach($results as $emigrant) {
921 \core\session\manager::touch_session($emigrant->session_id);
926 $end = ob_end_clean();
928 if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
929 return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
933 * Cron function will be called automatically by cron.php every 5 minutes
935 * @return void
937 function cron() {
938 global $DB;
940 // run the keepalive client
941 $this->keepalive_client();
943 $random100 = rand(0,100);
944 if ($random100 < 10) { // Approximately 10% of the time.
945 // nuke olden sessions
946 $longtime = time() - (1 * 3600 * 24);
947 $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
952 * Cleanup any remote mnet_sessions, kill the local mnet_session data
954 * This is called by require_logout in moodlelib
956 * @return void
958 function prelogout_hook() {
959 global $CFG, $USER;
961 if (!is_enabled_auth('mnet')) {
962 return;
965 // If the user is local to this Moodle:
966 if ($USER->mnethostid == $this->mnet->id) {
967 $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
969 // Else the user has hit 'logout' at a Service Provider Moodle:
970 } else {
971 $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
977 * The SP uses this function to kill the session on the parent IdP
979 * @param string $username Username for session to kill
980 * @param string $useragent SHA1 hash of user agent to look for
981 * @return string A plaintext report of what has happened
983 function kill_parent($username, $useragent) {
984 global $CFG, $USER, $DB;
986 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
987 $sql = "
988 select
990 from
991 {mnet_session} s
992 where
993 s.username = ? AND
994 s.useragent = ? AND
995 s.mnethostid = ?";
997 $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
999 $ignore = $DB->delete_records('mnet_session',
1000 array('username'=>$username,
1001 'useragent'=>$useragent,
1002 'mnethostid'=>$USER->mnethostid));
1004 if (false != $mnetsessions) {
1005 $mnet_peer = new mnet_peer();
1006 $mnet_peer->set_id($USER->mnethostid);
1008 $mnet_request = new mnet_xmlrpc_client();
1009 $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1011 // set $token and $useragent parameters
1012 $mnet_request->add_param($username);
1013 $mnet_request->add_param($useragent);
1014 if ($mnet_request->send($mnet_peer) === false) {
1015 debugging(join("\n", $mnet_request->error));
1016 return false;
1020 return true;
1024 * The IdP uses this function to kill child sessions on other hosts
1026 * @param string $username Username for session to kill
1027 * @param string $useragent SHA1 hash of user agent to look for
1028 * @return string A plaintext report of what has happened
1030 function kill_children($username, $useragent) {
1031 global $CFG, $USER, $DB;
1032 $remoteclient = null;
1033 if (defined('MNET_SERVER')) {
1034 $remoteclient = get_mnet_remote_client();
1036 require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1038 $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1040 $returnstring = '';
1042 $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1044 if (false == $mnetsessions) {
1045 $returnstring .= "Could find no remote sessions\n";
1046 $mnetsessions = array();
1049 foreach($mnetsessions as $mnetsession) {
1050 // If this script is being executed by a remote peer, that means the user has clicked
1051 // logout on that peer, and the session on that peer can be deleted natively.
1052 // Skip over it.
1053 if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1054 continue;
1056 $returnstring .= "Deleting session\n";
1058 $mnet_peer = new mnet_peer();
1059 $mnet_peer->set_id($mnetsession->mnethostid);
1061 $mnet_request = new mnet_xmlrpc_client();
1062 $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1064 // set $token and $useragent parameters
1065 $mnet_request->add_param($username);
1066 $mnet_request->add_param($useragent);
1067 if ($mnet_request->send($mnet_peer) === false) {
1068 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1069 join("\n", $mnet_request->error));
1073 $ignore = $DB->delete_records('mnet_session',
1074 array('useragent'=>$useragent, 'userid'=>$userid));
1076 if (isset($remoteclient) && isset($remoteclient->id)) {
1077 \core\session\manager::kill_user_sessions($userid);
1079 return $returnstring;
1083 * When the IdP requests that child sessions are terminated,
1084 * this function will be called on each of the child hosts. The machine that
1085 * calls the function (over xmlrpc) provides us with the mnethostid we need.
1087 * @param string $username Username for session to kill
1088 * @param string $useragent SHA1 hash of user agent to look for
1089 * @return bool True on success
1091 function kill_child($username, $useragent) {
1092 global $CFG, $DB;
1093 $remoteclient = get_mnet_remote_client();
1094 $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1095 $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1096 if (false != $session) {
1097 \core\session\manager::kill_session($session->session_id);
1098 return true;
1100 return false;
1104 * To delete a host, we must delete all current sessions that users from
1105 * that host are currently engaged in.
1107 * @param string $sessionidarray An array of session hashes
1108 * @return bool True on success
1110 function end_local_sessions(&$sessionArray) {
1111 global $CFG;
1112 if (is_array($sessionArray)) {
1113 while($session = array_pop($sessionArray)) {
1114 \core\session\manager::kill_session($session->session_id);
1116 return true;
1118 return false;
1122 * Returns the user's profile image info
1124 * If the user exists and has a profile picture, the returned array will contain keys:
1125 * f1 - the content of the default 100x100px image
1126 * f1_mimetype - the mimetype of the f1 file
1127 * f2 - the content of the 35x35px variant of the image
1128 * f2_mimetype - the mimetype of the f2 file
1130 * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1132 * @see process_new_icon()
1133 * @uses mnet_remote_client callable via MNet XML-RPC
1134 * @param int $userid The id of the user
1135 * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1137 function fetch_user_image($username) {
1138 global $CFG, $DB;
1140 if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
1141 $fs = get_file_storage();
1142 $usercontext = context_user::instance($user->id, MUST_EXIST);
1143 $return = array();
1144 if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
1145 $return['f1'] = base64_encode($f1->get_content());
1146 $return['f1_mimetype'] = $f1->get_mimetype();
1147 } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
1148 $return['f1'] = base64_encode($f1->get_content());
1149 $return['f1_mimetype'] = $f1->get_mimetype();
1151 if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
1152 $return['f2'] = base64_encode($f2->get_content());
1153 $return['f2_mimetype'] = $f2->get_mimetype();
1154 } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
1155 $return['f2'] = base64_encode($f2->get_content());
1156 $return['f2_mimetype'] = $f2->get_mimetype();
1158 return $return;
1160 return false;
1164 * Returns the theme information and logo url as strings.
1166 * @return string The theme info
1168 function fetch_theme_info() {
1169 global $CFG;
1171 $themename = "$CFG->theme";
1172 $logourl = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1174 $return['themename'] = $themename;
1175 $return['logourl'] = $logourl;
1176 return $return;
1180 * Determines if an MNET host is providing the nominated service.
1182 * @param int $mnethostid The id of the remote host
1183 * @param string $servicename The name of the service
1184 * @return bool Whether the service is available on the remote host
1186 function has_service($mnethostid, $servicename) {
1187 global $CFG, $DB;
1189 $sql = "
1190 SELECT
1191 svc.id as serviceid,
1192 svc.name,
1193 svc.description,
1194 svc.offer,
1195 svc.apiversion,
1196 h2s.id as h2s_id
1197 FROM
1198 {mnet_host} h,
1199 {mnet_service} svc,
1200 {mnet_host2service} h2s
1201 WHERE
1202 h.deleted = '0' AND
1203 h.id = h2s.hostid AND
1204 h2s.hostid = ? AND
1205 h2s.serviceid = svc.id AND
1206 svc.name = ? AND
1207 h2s.subscribe = '1'";
1209 return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1213 * Checks the MNET access control table to see if the username/mnethost
1214 * is permitted to login to this moodle.
1216 * @param string $username The username
1217 * @param int $mnethostid The id of the remote mnethost
1218 * @return bool Whether the user can login from the remote host
1220 function can_login_remotely($username, $mnethostid) {
1221 global $DB;
1223 $accessctrl = 'allow';
1224 $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1225 if (!empty($aclrecord)) {
1226 $accessctrl = $aclrecord->accessctrl;
1228 return $accessctrl == 'allow';
1231 function logoutpage_hook() {
1232 global $USER, $CFG, $redirect, $DB;
1234 if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1235 $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1236 $redirect = $host->wwwroot.'/';
1241 * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1243 * @param object $logline The log information to be trimmed
1244 * @return object The passed logline object trimmed to not exceed storable limits
1246 function trim_logline ($logline) {
1247 $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1248 'url' => 255);
1249 foreach ($limits as $property => $limit) {
1250 if (isset($logline->$property)) {
1251 $logline->$property = substr($logline->$property, 0, $limit);
1255 return $logline;
1259 * Returns a list of potential IdPs that this authentication plugin supports.
1260 * This is used to provide links on the login page.
1262 * @param string $wantsurl the relative url fragment the user wants to get to. You can use this to compose a returnurl, for example
1264 * @return array like:
1265 * array(
1266 * array(
1267 * 'url' => 'http://someurl',
1268 * 'icon' => new pix_icon(...),
1269 * 'name' => get_string('somename', 'auth_yourplugin'),
1270 * ),
1273 function loginpage_idp_list($wantsurl) {
1274 global $DB, $CFG;
1276 // strip off wwwroot, since the remote site will prefix it's return url with this
1277 $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1279 $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1280 FROM {mnet_host} h
1281 JOIN {mnet_host2service} m ON h.id = m.hostid
1282 JOIN {mnet_service} s ON s.id = m.serviceid
1283 JOIN {mnet_application} a ON h.applicationid = a.id
1284 WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1285 $params = array('sso_sp', 0, 1);
1287 if (!empty($CFG->mnet_all_hosts_id)) {
1288 $sql .= " AND h.id <> ?";
1289 $params[] = $CFG->mnet_all_hosts_id;
1292 if (!$hosts = $DB->get_records_sql($sql, $params)) {
1293 return array();
1296 $idps = array();
1297 foreach ($hosts as $host) {
1298 $idps[] = array(
1299 'url' => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1300 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1301 'name' => $host->name,
1304 return $idps;