MDL-27107 auth_mnet - consider all incoming roaming users as confirmed
[moodle.git] / auth / mnet / auth.php
1 <?php
3 /**
4  * @author Martin Dougiamas
5  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
6  * @package moodle multiauth
7  *
8  * Authentication Plugin: Moodle Network Authentication
9  *
10  * Multiple host authentication support for Moodle Network.
11  *
12  * 2006-11-01  File created.
13  */
15 if (!defined('MOODLE_INTERNAL')) {
16     die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
17 }
19 require_once($CFG->libdir.'/authlib.php');
21 /**
22  * Moodle Network authentication plugin.
23  */
24 class auth_plugin_mnet extends auth_plugin_base {
26     /**
27      * Constructor.
28      */
29     function auth_plugin_mnet() {
30         $this->authtype = 'mnet';
31         $this->config = get_config('auth_mnet');
32         $this->mnet = get_mnet_environment();
33     }
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.
39      *
40      * @param string $username The username
41      * @param string $password The password
42      * @return bool Authentication success or failure.
43      */
44     function user_login($username, $password) {
45         return false; // print_error("mnetlocal");
46     }
48     /**
49      * Return user data for the provided token, compare with user_agent string.
50      *
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
54      */
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');
63         }
65         // check session confirm timeout
66         if ($mnet_session->confirm_timeout < time()) {
67             throw new mnet_server_exception(2, 'authfail_sessiontimedout');
68         }
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');
73         }
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();
91             }
92         }
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));
97         }
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);
110             }
111         }
113         return $userdata;
114     }
116     /**
117      * Generate a random string for use as an RPC session token.
118      */
119     function generate_token() {
120         return sha1(str_shuffle('' . mt_rand() . time()));
121     }
123     /**
124      * Starts an RPC jump session and returns the jump redirect URL.
125      *
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
130      */
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');
141         }
143         // check for SSO publish permission first
144         if ($this->has_service($mnethostid, 'sso_sp') == false) {
145             print_error('hostnotconfiguredforsso', 'mnet');
146         }
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');
152         }
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);
180         }
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';
188         }
190         return $url;
191     }
193     /**
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.
197      *
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.
201      */
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');
210         }
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;
230                 }
231                 $message .= "ERROR $code:<br/>$errormessage<br/>";
232             }
233             print_error("rpcerror", '', '', $message);
234         }
235         unset($mnetrequest);
237         if (empty($remoteuser) or empty($remoteuser->username)) {
238             print_error('unknownerror', 'mnet');
239             exit;
240         }
242         if (user_not_fully_set_up($remoteuser)) {
243             print_error('notenoughidpinfo', 'mnet');
244             exit;
245         }
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);
256         }
257         if (empty($remoteuser->lang)) {
258             if (!empty($CFG->lang)) {
259                 $remoteuser->lang = $CFG->lang;
260             } else {
261                 $remoteuser->lang = 'en';
262             }
263         }
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) {
272             /*
273             if (empty($this->config->auto_add_remote_users)) {
274                 print_error('nolocaluser', 'mnet');
275             } See MDL-21327   for why this is commented out
276             */
277             $remoteuser->mnethostid = $remotehost->id;
278             $remoteuser->firstaccess = time(); // First time user in this server, grab it here
279             $remoteuser->confirmed = 1;
281             $remoteuser->id = $DB->insert_record('user', $remoteuser);
282             $firsttime = true;
283             $localuser = $remoteuser;
284         }
286         // check sso access control list for permission first
287         if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
288             print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
289         }
291         $fs = get_file_storage();
293         // update the local user record with remote user data
294         foreach ((array) $remoteuser as $key => $val) {
296             if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
297                 // update the user picture if there is a newer verion at the identity provider
298                 $usercontext = get_context_instance(CONTEXT_USER, $localuser->id, MUST_EXIST);
299                 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
300                     $localtimemodified = $usericonfile->get_timemodified();
301                 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
302                     $localtimemodified = $usericonfile->get_timemodified();
303                 } else {
304                     $localtimemodified = 0;
305                 }
307                 if (!empty($val) and $localtimemodified < $val) {
308                     mnet_debug('refetching the user picture from the identity provider host');
309                     $fetchrequest = new mnet_xmlrpc_client();
310                     $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
311                     $fetchrequest->add_param($localuser->username);
312                     if ($fetchrequest->send($remotepeer) === true) {
313                         if (strlen($fetchrequest->response['f1']) > 0) {
314                             $imagefilename = $CFG->dataroot . '/temp/mnet-usericon-' . $localuser->id;
315                             $imagecontents = base64_decode($fetchrequest->response['f1']);
316                             file_put_contents($imagefilename, $imagecontents);
317                             if (process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
318                                 $localuser->picture = 1;
319                             }
320                             unlink($imagefilename);
321                         }
322                         // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
323                         // the mimetype information provided is ignored and the type of the file is detected
324                         // by process_new_icon()
325                     }
326                 }
327             }
329             if($key == 'myhosts') {
330                 $localuser->mnet_foreign_host_array = array();
331                 foreach($val as $rhost) {
332                     $name  = clean_param($rhost['name'], PARAM_ALPHANUM);
333                     $url   = clean_param($rhost['url'], PARAM_URL);
334                     $count = clean_param($rhost['count'], PARAM_INT);
335                     $url_is_local = stristr($url , $CFG->wwwroot);
336                     if (!empty($name) && !empty($count) && empty($url_is_local)) {
337                         $localuser->mnet_foreign_host_array[] = array('name'  => $name,
338                                                                       'url'   => $url,
339                                                                       'count' => $count);
340                     }
341                 }
342             }
344             $localuser->{$key} = $val;
345         }
347         $localuser->mnethostid = $remotepeer->id;
348         if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
349             $localuser->firstaccess = time();
350         }
352         $DB->update_record('user', $localuser);
354         if (!$firsttime) {
355             // repeat customer! let the IDP know about enrolments
356             // we have for this user.
357             // set up the RPC request
358             $mnetrequest = new mnet_xmlrpc_client();
359             $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
361             // pass username and an assoc array of "my courses"
362             // with info so that the IDP can maintain mnetservice_enrol_enrolments
363             $mnetrequest->add_param($remoteuser->username);
364             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
365             $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
366             if (is_array($courses) && !empty($courses)) {
367                 // Second request to do the JOINs that we'd have done
368                 // inside enrol_get_users_courses() if we had been allowed
369                 $sql = "SELECT c.id,
370                                cc.name AS cat_name, cc.description AS cat_description
371                           FROM {course} c
372                           JOIN {course_categories} cc ON c.category = cc.id
373                          WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
374                 $extra = $DB->get_records_sql($sql);
376                 $keys = array_keys($courses);
377                 $defaultrole = reset(get_archetype_roles('student'));
378                 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
379                 foreach ($keys AS $id) {
380                     if ($courses[$id]->visible == 0) {
381                         unset($courses[$id]);
382                         continue;
383                     }
384                     $courses[$id]->cat_id          = $courses[$id]->category;
385                     $courses[$id]->defaultroleid   = $defaultrole->id;
386                     unset($courses[$id]->category);
387                     unset($courses[$id]->visible);
389                     $courses[$id]->cat_name        = $extra[$id]->cat_name;
390                     $courses[$id]->cat_description = $extra[$id]->cat_description;
391                     $courses[$id]->defaultrolename = $defaultrole->name;
392                     // coerce to array
393                     $courses[$id] = (array)$courses[$id];
394                 }
395             } else {
396                 // if the array is empty, send it anyway
397                 // we may be clearing out stale entries
398                 $courses = array();
399             }
400             $mnetrequest->add_param($courses);
402             // Call 0800-RPC Now! -- we don't care too much if it fails
403             // as it's just informational.
404             if ($mnetrequest->send($remotepeer) === false) {
405                 // error_log(print_r($mnetrequest->error,1));
406             }
407         }
409         return $localuser;
410     }
413     /**
414      * creates (or updates) the mnet session once
415      * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
416      *
417      * @param stdclass  $user the local user (must exist already
418      * @param string    $token the jump/land token
419      * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
420      */
421     public function update_mnet_session($user, $token, $remotepeer) {
422         global $DB;
423         $session_gc_maxlifetime = 1440;
424         if (isset($user->session_gc_maxlifetime)) {
425             $session_gc_maxlifetime = $user->session_gc_maxlifetime;
426         }
427         if (!$mnet_session = $DB->get_record('mnet_session',
428                                    array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
429                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
430             $mnet_session = new stdClass();
431             $mnet_session->mnethostid = $remotepeer->id;
432             $mnet_session->userid = $user->id;
433             $mnet_session->username = $user->username;
434             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
435             $mnet_session->token = $token; // Needed to support simultaneous sessions
436                                            // and preserving DB rec uniqueness
437             $mnet_session->confirm_timeout = time();
438             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
439             $mnet_session->session_id = session_id();
440             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
441         } else {
442             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
443             $DB->update_record('mnet_session', $mnet_session);
444         }
445     }
449     /**
450      * Invoke this function _on_ the IDP to update it with enrolment info local to
451      * the SP right after calling user_authorise()
452      *
453      * Normally called by the SP after calling user_authorise()
454      *
455      * @param string $username The username
456      * @param array $courses  Assoc array of courses following the structure of mnetservice_enrol_courses
457      * @return bool
458      */
459     function update_enrolments($username, $courses) {
460         global $CFG, $DB;
461         $remoteclient = get_mnet_remote_client();
463         if (empty($username) || !is_array($courses)) {
464             return false;
465         }
466         // make sure it is a user we have an in active session
467         // with that host...
468         $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
469         $userid = null;
470         foreach ($mnetsessions as $mnetsession) {
471             if (is_null($userid)) {
472                 $userid = $mnetsession->userid;
473                 continue;
474             }
475             if ($userid != $mnetsession->userid) {
476                 throw new mnet_server_exception(3, 'authfail_usermismatch');
477             }
478         }
480         if (empty($courses)) { // no courses? clear out quickly
481             $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
482             return true;
483         }
485         // IMPORTANT: Ask for remoteid as the first element in the query, so
486         // that the array that comes back is indexed on the same field as the
487         // array that we have received from the remote client
488         $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
489                        c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
490                        e.id AS enrolmentid
491                   FROM {mnetservice_enrol_courses} c
492              LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
493                  WHERE e.userid = ? AND c.hostid = ?";
495         $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
497         $local_courseid_array = array();
498         foreach($courses as $ix => $course) {
500             $course['remoteid'] = $course['id'];
501             $course['hostid']   =  (int)$remoteclient->id;
502             $userisregd         = false;
504             // if we do not have the the information about the remote course, it is not available
505             // to us for remote enrolment - skip
506             if (array_key_exists($course['remoteid'], $currentcourses)) {
507                 // Pointer to current course:
508                 $currentcourse =& $currentcourses[$course['remoteid']];
509                 // We have a record - is it up-to-date?
510                 $course['id'] = $currentcourse->id;
512                 $saveflag = false;
514                 foreach($course as $key => $value) {
515                     if ($currentcourse->$key != $value) {
516                         $saveflag = true;
517                         $currentcourse->$key = $value;
518                     }
519                 }
521                 if ($saveflag) {
522                     $DB->update_record('mnetervice_enrol_courses', $currentcourse);
523                 }
525                 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
526                     $userisregd = true;
527                 }
528             } else {
529                 unset ($courses[$ix]);
530                 continue;
531             }
533             // By this point, we should always have a $dataObj->id
534             $local_courseid_array[] = $course['id'];
536             // Do we have a record for this assignment?
537             if ($userisregd) {
538                 // Yes - we know about this one already
539                 // We don't want to do updates because the new data is probably
540                 // 'less complete' than the data we have.
541             } else {
542                 // No - create a record
543                 $assignObj = new stdClass();
544                 $assignObj->userid    = $userid;
545                 $assignObj->hostid    = (int)$remoteclient->id;
546                 $assignObj->remotecourseid = $course['remoteid'];
547                 $assignObj->rolename  = $course['defaultrolename'];
548                 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
549             }
550         }
552         // Clean up courses that the user is no longer enrolled in.
553         if (!empty($local_courseid_array)) {
554             $local_courseid_string = implode(', ', $local_courseid_array);
555             $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
556             $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
557         }
558     }
560     function prevent_local_passwords() {
561         return true;
562     }
564     /**
565      * Returns true if this authentication plugin is 'internal'.
566      *
567      * @return bool
568      */
569     function is_internal() {
570         return false;
571     }
573     /**
574      * Returns true if this authentication plugin can change the user's
575      * password.
576      *
577      * @return bool
578      */
579     function can_change_password() {
580         //TODO: it should be able to redirect, right?
581         return false;
582     }
584     /**
585      * Returns the URL for changing the user's pw, or false if the default can
586      * be used.
587      *
588      * @return moodle_url
589      */
590     function change_password_url() {
591         return null;
592     }
594     /**
595      * Prints a form for configuring this authentication plugin.
596      *
597      * This function is called from admin/auth.php, and outputs a full page with
598      * a form for configuring this plugin.
599      *
600      * @param object $config
601      * @param object $err
602      * @param array $user_fields
603      */
604     function config_form($config, $err, $user_fields) {
605         global $CFG, $DB;
607          $query = "
608             SELECT
609                 h.id,
610                 h.name as hostname,
611                 h.wwwroot,
612                 h2idp.publish as idppublish,
613                 h2idp.subscribe as idpsubscribe,
614                 idp.name as idpname,
615                 h2sp.publish as sppublish,
616                 h2sp.subscribe as spsubscribe,
617                 sp.name as spname
618             FROM
619                 {mnet_host} h
620             LEFT JOIN
621                 {mnet_host2service} h2idp
622             ON
623                (h.id = h2idp.hostid AND
624                (h2idp.publish = 1 OR
625                 h2idp.subscribe = 1))
626             INNER JOIN
627                 {mnet_service} idp
628             ON
629                (h2idp.serviceid = idp.id AND
630                 idp.name = 'sso_idp')
631             LEFT JOIN
632                 {mnet_host2service} h2sp
633             ON
634                (h.id = h2sp.hostid AND
635                (h2sp.publish = 1 OR
636                 h2sp.subscribe = 1))
637             INNER JOIN
638                 {mnet_service} sp
639             ON
640                (h2sp.serviceid = sp.id AND
641                 sp.name = 'sso_sp')
642             WHERE
643                ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
644                (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
645                 h.id != ?
646             ORDER BY
647                 h.name ASC";
649         $id_providers       = array();
650         $service_providers  = array();
651         if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
652             foreach($resultset as $hostservice) {
653                 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
654                     $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
655                 }
656                 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
657                     $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
658                 }
659             }
660         }
662         include "config.html";
663     }
665     /**
666      * Processes and stores configuration data for this authentication plugin.
667      */
668     function process_config($config) {
669         // set to defaults if undefined
670         if (!isset ($config->rpc_negotiation_timeout)) {
671             $config->rpc_negotiation_timeout = '30';
672         }
673         /*
674         if (!isset ($config->auto_add_remote_users)) {
675             $config->auto_add_remote_users = '0';
676         } See MDL-21327   for why this is commented out
677         set_config('auto_add_remote_users',   $config->auto_add_remote_users,   'auth_mnet');
678         */
680         // save settings
681         set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
683         return true;
684     }
686     /**
687      * Poll the IdP server to let it know that a user it has authenticated is still
688      * online
689      *
690      * @return  void
691      */
692     function keepalive_client() {
693         global $CFG, $DB;
694         $cutoff = time() - 300; // TODO - find out what the remote server's session
695                                 // cutoff is, and preempt that
697         $sql = "
698             select
699                 id,
700                 username,
701                 mnethostid
702             from
703                 {user}
704             where
705                 lastaccess > ? AND
706                 mnethostid != ?
707             order by
708                 mnethostid";
710         $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
712         if ($immigrants == false) {
713             return true;
714         }
716         $usersArray = array();
717         foreach($immigrants as $immigrant) {
718             $usersArray[$immigrant->mnethostid][] = $immigrant->username;
719         }
721         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
722         foreach($usersArray as $mnethostid => $users) {
723             $mnet_peer = new mnet_peer();
724             $mnet_peer->set_id($mnethostid);
726             $mnet_request = new mnet_xmlrpc_client();
727             $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
729             // set $token and $useragent parameters
730             $mnet_request->add_param($users);
732             if ($mnet_request->send($mnet_peer) === true) {
733                 if (!isset($mnet_request->response['code'])) {
734                     debugging("Server side error has occured on host $mnethostid");
735                     continue;
736                 } elseif ($mnet_request->response['code'] > 0) {
737                     debugging($mnet_request->response['message']);
738                 }
740                 if (!isset($mnet_request->response['last log id'])) {
741                     debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
742                     continue;
743                 }
744             } else {
745                 debugging("Server side error has occured on host $mnethostid: " .
746                           join("\n", $mnet_request->error));
747                 break;
748             }
749             $mnethostlogssql = "
750             SELECT
751                 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
752                 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
753                 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
754                 c.modinfo
755             FROM
756                 (
757                     SELECT
758                         l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
759                         l.action, l.url, l.info, u.username
760                     FROM
761                         {user} u
762                         INNER JOIN {log} l on l.userid = u.id
763                     WHERE
764                         u.mnethostid = ?
765                         AND l.id > ?
766                     ORDER BY remoteid ASC
767                     LIMIT 500
768                 ) mhostlogs
769                 INNER JOIN {course} c on c.id = mhostlogs.course
770             ORDER by mhostlogs.remoteid ASC";
772             $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
774             if ($mnethostlogs == false) {
775                 continue;
776             }
778             $processedlogs = array();
780             foreach($mnethostlogs as $hostlog) {
781                 // Extract the name of the relevant module instance from the
782                 // course modinfo if possible.
783                 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
784                     $modinfo = unserialize($hostlog->modinfo);
785                     unset($hostlog->modinfo);
786                     $modulearray = array();
787                     foreach($modinfo as $module) {
788                         $modulearray[$module->cm] = $module->name;
789                     }
790                     $hostlog->resource_name = $modulearray[$hostlog->cmid];
791                 } else {
792                     $hostlog->resource_name = '';
793                 }
795                 $processedlogs[] = array (
796                                     'remoteid'      => $hostlog->remoteid,
797                                     'time'          => $hostlog->time,
798                                     'userid'        => $hostlog->userid,
799                                     'ip'            => $hostlog->ip,
800                                     'course'        => $hostlog->course,
801                                     'coursename'    => $hostlog->coursename,
802                                     'module'        => $hostlog->module,
803                                     'cmid'          => $hostlog->cmid,
804                                     'action'        => $hostlog->action,
805                                     'url'           => $hostlog->url,
806                                     'info'          => $hostlog->info,
807                                     'resource_name' => $hostlog->resource_name,
808                                     'username'      => $hostlog->username
809                                  );
810             }
812             unset($hostlog);
814             $mnet_request = new mnet_xmlrpc_client();
815             $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
817             // set $token and $useragent parameters
818             $mnet_request->add_param($processedlogs);
820             if ($mnet_request->send($mnet_peer) === true) {
821                 if ($mnet_request->response['code'] > 0) {
822                     debugging($mnet_request->response['message']);
823                 }
824             } else {
825                 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
826             }
827         }
828     }
830     /**
831      * Receives an array of log entries from an SP and adds them to the mnet_log
832      * table
833      *
834      * @param   array   $array      An array of usernames
835      * @return  string              "All ok" or an error message
836      */
837     function refresh_log($array) {
838         global $CFG, $DB;
839         $remoteclient = get_mnet_remote_client();
841         // We don't want to output anything to the client machine
842         $start = ob_start();
844         $returnString = '';
845         $transaction = $DB->start_delegated_transaction();
846         $useridarray = array();
848         foreach($array as $logEntry) {
849             $logEntryObj = (object)$logEntry;
850             $logEntryObj->hostid = $remoteclient->id;
852             if (isset($useridarray[$logEntryObj->username])) {
853                 $logEntryObj->userid = $useridarray[$logEntryObj->username];
854             } else {
855                 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
856                 if ($logEntryObj->userid == false) {
857                     $logEntryObj->userid = 0;
858                 }
859                 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
860             }
862             unset($logEntryObj->username);
864             $logEntryObj = $this->trim_logline($logEntryObj);
865             $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
867             if ($insertok) {
868                 $remoteclient->last_log_id = $logEntryObj->remoteid;
869             } else {
870                 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
871             }
872         }
873         $remoteclient->commit();
874         $transaction->allow_commit();
876         $end = ob_end_clean();
878         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
879         return array('code' => 1, 'message' => $returnString);
880     }
882     /**
883      * Receives an array of usernames from a remote machine and prods their
884      * sessions to keep them alive
885      *
886      * @param   array   $array      An array of usernames
887      * @return  string              "All ok" or an error message
888      */
889     function keepalive_server($array) {
890         global $CFG, $DB;
891         $remoteclient = get_mnet_remote_client();
893         // We don't want to output anything to the client machine
894         $start = ob_start();
896         // We'll get session records in batches of 30
897         $superArray = array_chunk($array, 30);
899         $returnString = '';
901         foreach($superArray as $subArray) {
902             $subArray = array_values($subArray);
903             $instring = "('".implode("', '",$subArray)."')";
904             $query = "select id, session_id, username from {mnet_session} where username in $instring";
905             $results = $DB->get_records_sql($query);
907             if ($results == false) {
908                 // We seem to have a username that breaks our query:
909                 // TODO: Handle this error appropriately
910                 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
911             } else {
912                 foreach($results as $emigrant) {
913                     session_touch($emigrant->session_id);
914                 }
915             }
916         }
918         $end = ob_end_clean();
920         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
921         return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
922     }
924     /**
925      * Cron function will be called automatically by cron.php every 5 minutes
926      *
927      * @return void
928      */
929     function cron() {
930         global $DB;
932         // run the keepalive client
933         $this->keepalive_client();
935         // admin/cron.php should have run srand for us
936         $random100 = rand(0,100);
937         if ($random100 < 10) {     // Approximately 10% of the time.
938             // nuke olden sessions
939             $longtime = time() - (1 * 3600 * 24);
940             $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
941         }
942     }
944     /**
945      * Cleanup any remote mnet_sessions, kill the local mnet_session data
946      *
947      * This is called by require_logout in moodlelib
948      *
949      * @return   void
950      */
951     function prelogout_hook() {
952         global $CFG, $USER;
954         if (!is_enabled_auth('mnet')) {
955             return;
956         }
958         // If the user is local to this Moodle:
959         if ($USER->mnethostid == $this->mnet->id) {
960             $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
962         // Else the user has hit 'logout' at a Service Provider Moodle:
963         } else {
964             $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
966         }
967     }
969     /**
970      * The SP uses this function to kill the session on the parent IdP
971      *
972      * @param   string  $username       Username for session to kill
973      * @param   string  $useragent      SHA1 hash of user agent to look for
974      * @return  string                  A plaintext report of what has happened
975      */
976     function kill_parent($username, $useragent) {
977         global $CFG, $USER, $DB;
979         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
980         $sql = "
981             select
982                 *
983             from
984                 {mnet_session} s
985             where
986                 s.username   = ? AND
987                 s.useragent  = ? AND
988                 s.mnethostid = ?";
990         $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
992         $ignore = $DB->delete_records('mnet_session',
993                                  array('username'=>$username,
994                                  'useragent'=>$useragent,
995                                  'mnethostid'=>$USER->mnethostid));
997         if (false != $mnetsessions) {
998             $mnet_peer = new mnet_peer();
999             $mnet_peer->set_id($USER->mnethostid);
1001             $mnet_request = new mnet_xmlrpc_client();
1002             $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1004             // set $token and $useragent parameters
1005             $mnet_request->add_param($username);
1006             $mnet_request->add_param($useragent);
1007             if ($mnet_request->send($mnet_peer) === false) {
1008                 debugging(join("\n", $mnet_request->error));
1009                 return false;
1010             }
1011         }
1013         return true;
1014     }
1016     /**
1017      * The IdP uses this function to kill child sessions on other hosts
1018      *
1019      * @param   string  $username       Username for session to kill
1020      * @param   string  $useragent      SHA1 hash of user agent to look for
1021      * @return  string                  A plaintext report of what has happened
1022      */
1023     function kill_children($username, $useragent) {
1024         global $CFG, $USER, $DB;
1025         $remoteclient = null;
1026         if (defined('MNET_SERVER')) {
1027             $remoteclient = get_mnet_remote_client();
1028         }
1029         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1031         $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1033         $returnstring = '';
1035         $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1037         if (false == $mnetsessions) {
1038             $returnstring .= "Could find no remote sessions\n";
1039             $mnetsessions = array();
1040         }
1042         foreach($mnetsessions as $mnetsession) {
1043             // If this script is being executed by a remote peer, that means the user has clicked
1044             // logout on that peer, and the session on that peer can be deleted natively.
1045             // Skip over it.
1046             if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1047                 continue;
1048             }
1049             $returnstring .=  "Deleting session\n";
1051             $mnet_peer = new mnet_peer();
1052             $mnet_peer->set_id($mnetsession->mnethostid);
1054             $mnet_request = new mnet_xmlrpc_client();
1055             $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1057             // set $token and $useragent parameters
1058             $mnet_request->add_param($username);
1059             $mnet_request->add_param($useragent);
1060             if ($mnet_request->send($mnet_peer) === false) {
1061                 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1062                           join("\n", $mnet_request->error));
1063             }
1064         }
1066         $ignore = $DB->delete_records('mnet_session',
1067                                  array('useragent'=>$useragent, 'userid'=>$userid));
1069         if (isset($remoteclient) && isset($remoteclient->id)) {
1070             session_kill_user($userid);
1071         }
1072         return $returnstring;
1073     }
1075     /**
1076      * When the IdP requests that child sessions are terminated,
1077      * this function will be called on each of the child hosts. The machine that
1078      * calls the function (over xmlrpc) provides us with the mnethostid we need.
1079      *
1080      * @param   string  $username       Username for session to kill
1081      * @param   string  $useragent      SHA1 hash of user agent to look for
1082      * @return  bool                    True on success
1083      */
1084     function kill_child($username, $useragent) {
1085         global $CFG, $DB;
1086         $remoteclient = get_mnet_remote_client();
1087         $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1088         $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1089         if (false != $session) {
1090             session_kill($session->session_id);
1091             return true;
1092         }
1093         return false;
1094     }
1096     /**
1097      * To delete a host, we must delete all current sessions that users from
1098      * that host are currently engaged in.
1099      *
1100      * @param   string  $sessionidarray   An array of session hashes
1101      * @return  bool                      True on success
1102      */
1103     function end_local_sessions(&$sessionArray) {
1104         global $CFG;
1105         if (is_array($sessionArray)) {
1106             while($session = array_pop($sessionArray)) {
1107                 session_kill($session->session_id);
1108             }
1109             return true;
1110         }
1111         return false;
1112     }
1114     /**
1115      * Returns the user's profile image info
1116      *
1117      * If the user exists and has a profile picture, the returned array will contain keys:
1118      *  f1          - the content of the default 100x100px image
1119      *  f1_mimetype - the mimetype of the f1 file
1120      *  f2          - the content of the 35x35px variant of the image
1121      *  f2_mimetype - the mimetype of the f2 file
1122      *
1123      * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1124      *
1125      * @see process_new_icon()
1126      * @uses mnet_remote_client callable via MNet XML-RPC
1127      * @param int $userid The id of the user
1128      * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1129      */
1130     function fetch_user_image($username) {
1131         global $CFG, $DB;
1133         if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
1134             $fs = get_file_storage();
1135             $usercontext = get_context_instance(CONTEXT_USER, $user->id, MUST_EXIST);
1136             $return = array();
1137             if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
1138                 $return['f1'] = base64_encode($f1->get_content());
1139                 $return['f1_mimetype'] = $f1->get_mimetype();
1140             } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
1141                 $return['f1'] = base64_encode($f1->get_content());
1142                 $return['f1_mimetype'] = $f1->get_mimetype();
1143             }
1144             if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
1145                 $return['f2'] = base64_encode($f2->get_content());
1146                 $return['f2_mimetype'] = $f2->get_mimetype();
1147             } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
1148                 $return['f2'] = base64_encode($f2->get_content());
1149                 $return['f2_mimetype'] = $f2->get_mimetype();
1150             }
1151             return $return;
1152         }
1153         return false;
1154     }
1156     /**
1157      * Returns the theme information and logo url as strings.
1158      *
1159      * @return string     The theme info
1160      */
1161     function fetch_theme_info() {
1162         global $CFG;
1164         $themename = "$CFG->theme";
1165         $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1167         $return['themename'] = $themename;
1168         $return['logourl'] = $logourl;
1169         return $return;
1170     }
1172     /**
1173      * Determines if an MNET host is providing the nominated service.
1174      *
1175      * @param int    $mnethostid   The id of the remote host
1176      * @param string $servicename  The name of the service
1177      * @return bool                Whether the service is available on the remote host
1178      */
1179     function has_service($mnethostid, $servicename) {
1180         global $CFG, $DB;
1182         $sql = "
1183             SELECT
1184                 svc.id as serviceid,
1185                 svc.name,
1186                 svc.description,
1187                 svc.offer,
1188                 svc.apiversion,
1189                 h2s.id as h2s_id
1190             FROM
1191                 {mnet_host} h,
1192                 {mnet_service} svc,
1193                 {mnet_host2service} h2s
1194             WHERE
1195                 h.deleted = '0' AND
1196                 h.id = h2s.hostid AND
1197                 h2s.hostid = ? AND
1198                 h2s.serviceid = svc.id AND
1199                 svc.name = ? AND
1200                 h2s.subscribe = '1'";
1202         return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1203     }
1205     /**
1206      * Checks the MNET access control table to see if the username/mnethost
1207      * is permitted to login to this moodle.
1208      *
1209      * @param string $username   The username
1210      * @param int    $mnethostid The id of the remote mnethost
1211      * @return bool              Whether the user can login from the remote host
1212      */
1213     function can_login_remotely($username, $mnethostid) {
1214         global $DB;
1216         $accessctrl = 'allow';
1217         $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1218         if (!empty($aclrecord)) {
1219             $accessctrl = $aclrecord->accessctrl;
1220         }
1221         return $accessctrl == 'allow';
1222     }
1224     function logoutpage_hook() {
1225         global $USER, $CFG, $redirect, $DB;
1227         if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1228             $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1229             $redirect = $host->wwwroot.'/';
1230         }
1231     }
1233     /**
1234      * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1235      *
1236      * @param object $logline The log information to be trimmed
1237      * @return object The passed logline object trimmed to not exceed storable limits
1238      */
1239     function trim_logline ($logline) {
1240         $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1241                         'url' => 255);
1242         foreach ($limits as $property => $limit) {
1243             if (isset($logline->$property)) {
1244                 $logline->$property = substr($logline->$property, 0, $limit);
1245             }
1246         }
1248         return $logline;
1249     }
1251     /**
1252      * Returns a list of potential IdPs that this authentication plugin supports.
1253      * This is used to provide links on the login page.
1254      *
1255      * @param string $wantsurl the relative url fragment the user wants to get to.  You can use this to compose a returnurl, for example
1256      *
1257      * @return array like:
1258      *              array(
1259      *                  array(
1260      *                      'url' => 'http://someurl',
1261      *                      'icon' => new pix_icon(...),
1262      *                      'name' => get_string('somename', 'auth_yourplugin'),
1263      *                 ),
1264      *             )
1265      */
1266     function loginpage_idp_list($wantsurl) {
1267         global $DB, $CFG;
1269         // strip off wwwroot, since the remote site will prefix it's return url with this
1270         $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1272         $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1273                   FROM {mnet_host} h
1274                   JOIN {mnet_host2service} m ON h.id = m.hostid
1275                   JOIN {mnet_service} s ON s.id = m.serviceid
1276                   JOIN {mnet_application} a ON h.applicationid = a.id
1277                  WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1278         $params = array('sso_sp', 0, 1);
1280         if (!empty($CFG->mnet_all_hosts_id)) {
1281             $sql .= " AND h.id <> ?";
1282             $params[] = $CFG->mnet_all_hosts_id;
1283         }
1285         if (!$hosts = $DB->get_records_sql($sql, $params)) {
1286             return array();
1287         }
1289         $idps = array();
1290         foreach ($hosts as $host) {
1291             $idps[] = array(
1292                 'url'  => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1293                 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1294                 'name' => $host->name,
1295             );
1296         }
1297         return $idps;
1298     }