MNet: fixed wrong column name in SQL cleanup code
[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
280             $remoteuser->id = $DB->insert_record('user', $remoteuser);
281             $firsttime = true;
282             $localuser = $remoteuser;
283         }
285         // check sso access control list for permission first
286         if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
287             print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
288         }
290         $fs = get_file_storage();
292         // update the local user record with remote user data
293         foreach ((array) $remoteuser as $key => $val) {
295             if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
296                 // update the user picture if there is a newer verion at the identity provider
297                 $usercontext = get_context_instance(CONTEXT_USER, $localuser->id, MUST_EXIST);
298                 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
299                     $localtimemodified = $usericonfile->get_timemodified();
300                 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
301                     $localtimemodified = $usericonfile->get_timemodified();
302                 } else {
303                     $localtimemodified = 0;
304                 }
306                 if (!empty($val) and $localtimemodified < $val) {
307                     mnet_debug('refetching the user picture from the identity provider host');
308                     $fetchrequest = new mnet_xmlrpc_client();
309                     $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
310                     $fetchrequest->add_param($localuser->username);
311                     if ($fetchrequest->send($remotepeer) === true) {
312                         if (strlen($fetchrequest->response['f1']) > 0) {
313                             $imagefilename = $CFG->dataroot . '/temp/mnet-usericon-' . $localuser->id;
314                             $imagecontents = base64_decode($fetchrequest->response['f1']);
315                             file_put_contents($imagefilename, $imagecontents);
316                             if (process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
317                                 $localuser->picture = 1;
318                             }
319                             unlink($imagefilename);
320                         }
321                         // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
322                         // the mimetype information provided is ignored and the type of the file is detected
323                         // by process_new_icon()
324                     }
325                 }
326             }
328             if($key == 'myhosts') {
329                 $localuser->mnet_foreign_host_array = array();
330                 foreach($val as $rhost) {
331                     $name  = clean_param($rhost['name'], PARAM_ALPHANUM);
332                     $url   = clean_param($rhost['url'], PARAM_URL);
333                     $count = clean_param($rhost['count'], PARAM_INT);
334                     $url_is_local = stristr($url , $CFG->wwwroot);
335                     if (!empty($name) && !empty($count) && empty($url_is_local)) {
336                         $localuser->mnet_foreign_host_array[] = array('name'  => $name,
337                                                                       'url'   => $url,
338                                                                       'count' => $count);
339                     }
340                 }
341             }
343             $localuser->{$key} = $val;
344         }
346         $localuser->mnethostid = $remotepeer->id;
347         if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
348             $localuser->firstaccess = time();
349         }
351         $DB->update_record('user', $localuser);
353         if (!$firsttime) {
354             // repeat customer! let the IDP know about enrolments
355             // we have for this user.
356             // set up the RPC request
357             $mnetrequest = new mnet_xmlrpc_client();
358             $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
360             // pass username and an assoc array of "my courses"
361             // with info so that the IDP can maintain mnetservice_enrol_enrolments
362             $mnetrequest->add_param($remoteuser->username);
363             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
364             $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
365             if (is_array($courses) && !empty($courses)) {
366                 // Second request to do the JOINs that we'd have done
367                 // inside enrol_get_users_courses() if we had been allowed
368                 $sql = "SELECT c.id,
369                                cc.name AS cat_name, cc.description AS cat_description
370                           FROM {course} c
371                           JOIN {course_categories} cc ON c.category = cc.id
372                          WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
373                 $extra = $DB->get_records_sql($sql);
375                 $keys = array_keys($courses);
376                 $defaultrole = reset(get_archetype_roles('student'));
377                 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
378                 foreach ($keys AS $id) {
379                     if ($courses[$id]->visible == 0) {
380                         unset($courses[$id]);
381                         continue;
382                     }
383                     $courses[$id]->cat_id          = $courses[$id]->category;
384                     $courses[$id]->defaultroleid   = $defaultrole->id;
385                     unset($courses[$id]->category);
386                     unset($courses[$id]->visible);
388                     $courses[$id]->cat_name        = $extra[$id]->cat_name;
389                     $courses[$id]->cat_description = $extra[$id]->cat_description;
390                     $courses[$id]->defaultrolename = $defaultrole->name;
391                     // coerce to array
392                     $courses[$id] = (array)$courses[$id];
393                 }
394             } else {
395                 // if the array is empty, send it anyway
396                 // we may be clearing out stale entries
397                 $courses = array();
398             }
399             $mnetrequest->add_param($courses);
401             // Call 0800-RPC Now! -- we don't care too much if it fails
402             // as it's just informational.
403             if ($mnetrequest->send($remotepeer) === false) {
404                 // error_log(print_r($mnetrequest->error,1));
405             }
406         }
408         return $localuser;
409     }
412     /**
413      * creates (or updates) the mnet session once
414      * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
415      *
416      * @param stdclass  $user the local user (must exist already
417      * @param string    $token the jump/land token
418      * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
419      */
420     public function update_mnet_session($user, $token, $remotepeer) {
421         global $DB;
422         $session_gc_maxlifetime = 1440;
423         if (isset($user->session_gc_maxlifetime)) {
424             $session_gc_maxlifetime = $user->session_gc_maxlifetime;
425         }
426         if (!$mnet_session = $DB->get_record('mnet_session',
427                                    array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
428                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
429             $mnet_session = new stdClass();
430             $mnet_session->mnethostid = $remotepeer->id;
431             $mnet_session->userid = $user->id;
432             $mnet_session->username = $user->username;
433             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
434             $mnet_session->token = $token; // Needed to support simultaneous sessions
435                                            // and preserving DB rec uniqueness
436             $mnet_session->confirm_timeout = time();
437             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
438             $mnet_session->session_id = session_id();
439             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
440         } else {
441             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
442             $DB->update_record('mnet_session', $mnet_session);
443         }
444     }
448     /**
449      * Invoke this function _on_ the IDP to update it with enrolment info local to
450      * the SP right after calling user_authorise()
451      *
452      * Normally called by the SP after calling user_authorise()
453      *
454      * @param string $username The username
455      * @param array $courses  Assoc array of courses following the structure of mnetservice_enrol_courses
456      * @return bool
457      */
458     function update_enrolments($username, $courses) {
459         global $CFG, $DB;
460         $remoteclient = get_mnet_remote_client();
462         if (empty($username) || !is_array($courses)) {
463             return false;
464         }
465         // make sure it is a user we have an in active session
466         // with that host...
467         $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
468         $userid = null;
469         foreach ($mnetsessions as $mnetsession) {
470             if (is_null($userid)) {
471                 $userid = $mnetsession->userid;
472                 continue;
473             }
474             if ($userid != $mnetsession->userid) {
475                 throw new mnet_server_exception(3, 'authfail_usermismatch');
476             }
477         }
479         if (empty($courses)) { // no courses? clear out quickly
480             $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
481             return true;
482         }
484         // IMPORTANT: Ask for remoteid as the first element in the query, so
485         // that the array that comes back is indexed on the same field as the
486         // array that we have received from the remote client
487         $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
488                        c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
489                        e.id AS enrolmentid
490                   FROM {mnetservice_enrol_courses} c
491              LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
492                  WHERE e.userid = ? AND c.hostid = ?";
494         $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
496         $local_courseid_array = array();
497         foreach($courses as $ix => $course) {
499             $course['remoteid'] = $course['id'];
500             $course['hostid']   =  (int)$remoteclient->id;
501             $userisregd         = false;
503             // if we do not have the the information about the remote course, it is not available
504             // to us for remote enrolment - skip
505             if (array_key_exists($course['remoteid'], $currentcourses)) {
506                 // Pointer to current course:
507                 $currentcourse =& $currentcourses[$course['remoteid']];
508                 // We have a record - is it up-to-date?
509                 $course['id'] = $currentcourse->id;
511                 $saveflag = false;
513                 foreach($course as $key => $value) {
514                     if ($currentcourse->$key != $value) {
515                         $saveflag = true;
516                         $currentcourse->$key = $value;
517                     }
518                 }
520                 if ($saveflag) {
521                     $DB->update_record('mnetervice_enrol_courses', $currentcourse);
522                 }
524                 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
525                     $userisregd = true;
526                 }
527             } else {
528                 unset ($courses[$ix]);
529                 continue;
530             }
532             // By this point, we should always have a $dataObj->id
533             $local_courseid_array[] = $course['id'];
535             // Do we have a record for this assignment?
536             if ($userisregd) {
537                 // Yes - we know about this one already
538                 // We don't want to do updates because the new data is probably
539                 // 'less complete' than the data we have.
540             } else {
541                 // No - create a record
542                 $assignObj = new stdClass();
543                 $assignObj->userid    = $userid;
544                 $assignObj->hostid    = (int)$remoteclient->id;
545                 $assignObj->remotecourseid = $course['remoteid'];
546                 $assignObj->rolename  = $course['defaultrolename'];
547                 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
548             }
549         }
551         // Clean up courses that the user is no longer enrolled in.
552         if (!empty($local_courseid_array)) {
553             $local_courseid_string = implode(', ', $local_courseid_array);
554             $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
555             $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
556         }
557     }
559     function prevent_local_passwords() {
560         return true;
561     }
563     /**
564      * Returns true if this authentication plugin is 'internal'.
565      *
566      * @return bool
567      */
568     function is_internal() {
569         return false;
570     }
572     /**
573      * Returns true if this authentication plugin can change the user's
574      * password.
575      *
576      * @return bool
577      */
578     function can_change_password() {
579         //TODO: it should be able to redirect, right?
580         return false;
581     }
583     /**
584      * Returns the URL for changing the user's pw, or false if the default can
585      * be used.
586      *
587      * @return moodle_url
588      */
589     function change_password_url() {
590         return null;
591     }
593     /**
594      * Prints a form for configuring this authentication plugin.
595      *
596      * This function is called from admin/auth.php, and outputs a full page with
597      * a form for configuring this plugin.
598      *
599      * @param object $config
600      * @param object $err
601      * @param array $user_fields
602      */
603     function config_form($config, $err, $user_fields) {
604         global $CFG, $DB;
606          $query = "
607             SELECT
608                 h.id,
609                 h.name as hostname,
610                 h.wwwroot,
611                 h2idp.publish as idppublish,
612                 h2idp.subscribe as idpsubscribe,
613                 idp.name as idpname,
614                 h2sp.publish as sppublish,
615                 h2sp.subscribe as spsubscribe,
616                 sp.name as spname
617             FROM
618                 {mnet_host} h
619             LEFT JOIN
620                 {mnet_host2service} h2idp
621             ON
622                (h.id = h2idp.hostid AND
623                (h2idp.publish = 1 OR
624                 h2idp.subscribe = 1))
625             INNER JOIN
626                 {mnet_service} idp
627             ON
628                (h2idp.serviceid = idp.id AND
629                 idp.name = 'sso_idp')
630             LEFT JOIN
631                 {mnet_host2service} h2sp
632             ON
633                (h.id = h2sp.hostid AND
634                (h2sp.publish = 1 OR
635                 h2sp.subscribe = 1))
636             INNER JOIN
637                 {mnet_service} sp
638             ON
639                (h2sp.serviceid = sp.id AND
640                 sp.name = 'sso_sp')
641             WHERE
642                ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
643                (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
644                 h.id != ?
645             ORDER BY
646                 h.name ASC";
648         $id_providers       = array();
649         $service_providers  = array();
650         if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
651             foreach($resultset as $hostservice) {
652                 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
653                     $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
654                 }
655                 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
656                     $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
657                 }
658             }
659         }
661         include "config.html";
662     }
664     /**
665      * Processes and stores configuration data for this authentication plugin.
666      */
667     function process_config($config) {
668         // set to defaults if undefined
669         if (!isset ($config->rpc_negotiation_timeout)) {
670             $config->rpc_negotiation_timeout = '30';
671         }
672         /*
673         if (!isset ($config->auto_add_remote_users)) {
674             $config->auto_add_remote_users = '0';
675         } See MDL-21327   for why this is commented out
676         set_config('auto_add_remote_users',   $config->auto_add_remote_users,   'auth_mnet');
677         */
679         // save settings
680         set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
682         return true;
683     }
685     /**
686      * Poll the IdP server to let it know that a user it has authenticated is still
687      * online
688      *
689      * @return  void
690      */
691     function keepalive_client() {
692         global $CFG, $DB;
693         $cutoff = time() - 300; // TODO - find out what the remote server's session
694                                 // cutoff is, and preempt that
696         $sql = "
697             select
698                 id,
699                 username,
700                 mnethostid
701             from
702                 {user}
703             where
704                 lastaccess > ? AND
705                 mnethostid != ?
706             order by
707                 mnethostid";
709         $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
711         if ($immigrants == false) {
712             return true;
713         }
715         $usersArray = array();
716         foreach($immigrants as $immigrant) {
717             $usersArray[$immigrant->mnethostid][] = $immigrant->username;
718         }
720         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
721         foreach($usersArray as $mnethostid => $users) {
722             $mnet_peer = new mnet_peer();
723             $mnet_peer->set_id($mnethostid);
725             $mnet_request = new mnet_xmlrpc_client();
726             $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
728             // set $token and $useragent parameters
729             $mnet_request->add_param($users);
731             if ($mnet_request->send($mnet_peer) === true) {
732                 if (!isset($mnet_request->response['code'])) {
733                     debugging("Server side error has occured on host $mnethostid");
734                     continue;
735                 } elseif ($mnet_request->response['code'] > 0) {
736                     debugging($mnet_request->response['message']);
737                 }
739                 if (!isset($mnet_request->response['last log id'])) {
740                     debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
741                     continue;
742                 }
743             } else {
744                 debugging("Server side error has occured on host $mnethostid: " .
745                           join("\n", $mnet_request->error));
746                 break;
747             }
748             $mnethostlogssql = "
749             SELECT
750                 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
751                 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
752                 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
753                 c.modinfo
754             FROM
755                 (
756                     SELECT
757                         l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
758                         l.action, l.url, l.info, u.username
759                     FROM
760                         {user} u
761                         INNER JOIN {log} l on l.userid = u.id
762                     WHERE
763                         u.mnethostid = ?
764                         AND l.id > ?
765                     ORDER BY remoteid ASC
766                     LIMIT 500
767                 ) mhostlogs
768                 INNER JOIN {course} c on c.id = mhostlogs.course
769             ORDER by mhostlogs.remoteid ASC";
771             $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
773             if ($mnethostlogs == false) {
774                 continue;
775             }
777             $processedlogs = array();
779             foreach($mnethostlogs as $hostlog) {
780                 // Extract the name of the relevant module instance from the
781                 // course modinfo if possible.
782                 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
783                     $modinfo = unserialize($hostlog->modinfo);
784                     unset($hostlog->modinfo);
785                     $modulearray = array();
786                     foreach($modinfo as $module) {
787                         $modulearray[$module->cm] = $module->name;
788                     }
789                     $hostlog->resource_name = $modulearray[$hostlog->cmid];
790                 } else {
791                     $hostlog->resource_name = '';
792                 }
794                 $processedlogs[] = array (
795                                     'remoteid'      => $hostlog->remoteid,
796                                     'time'          => $hostlog->time,
797                                     'userid'        => $hostlog->userid,
798                                     'ip'            => $hostlog->ip,
799                                     'course'        => $hostlog->course,
800                                     'coursename'    => $hostlog->coursename,
801                                     'module'        => $hostlog->module,
802                                     'cmid'          => $hostlog->cmid,
803                                     'action'        => $hostlog->action,
804                                     'url'           => $hostlog->url,
805                                     'info'          => $hostlog->info,
806                                     'resource_name' => $hostlog->resource_name,
807                                     'username'      => $hostlog->username
808                                  );
809             }
811             unset($hostlog);
813             $mnet_request = new mnet_xmlrpc_client();
814             $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
816             // set $token and $useragent parameters
817             $mnet_request->add_param($processedlogs);
819             if ($mnet_request->send($mnet_peer) === true) {
820                 if ($mnet_request->response['code'] > 0) {
821                     debugging($mnet_request->response['message']);
822                 }
823             } else {
824                 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
825             }
826         }
827     }
829     /**
830      * Receives an array of log entries from an SP and adds them to the mnet_log
831      * table
832      *
833      * @param   array   $array      An array of usernames
834      * @return  string              "All ok" or an error message
835      */
836     function refresh_log($array) {
837         global $CFG, $DB;
838         $remoteclient = get_mnet_remote_client();
840         // We don't want to output anything to the client machine
841         $start = ob_start();
843         $returnString = '';
844         $transaction = $DB->start_delegated_transaction();
845         $useridarray = array();
847         foreach($array as $logEntry) {
848             $logEntryObj = (object)$logEntry;
849             $logEntryObj->hostid = $remoteclient->id;
851             if (isset($useridarray[$logEntryObj->username])) {
852                 $logEntryObj->userid = $useridarray[$logEntryObj->username];
853             } else {
854                 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
855                 if ($logEntryObj->userid == false) {
856                     $logEntryObj->userid = 0;
857                 }
858                 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
859             }
861             unset($logEntryObj->username);
863             $logEntryObj = $this->trim_logline($logEntryObj);
864             $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
866             if ($insertok) {
867                 $remoteclient->last_log_id = $logEntryObj->remoteid;
868             } else {
869                 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
870             }
871         }
872         $remoteclient->commit();
873         $transaction->allow_commit();
875         $end = ob_end_clean();
877         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
878         return array('code' => 1, 'message' => $returnString);
879     }
881     /**
882      * Receives an array of usernames from a remote machine and prods their
883      * sessions to keep them alive
884      *
885      * @param   array   $array      An array of usernames
886      * @return  string              "All ok" or an error message
887      */
888     function keepalive_server($array) {
889         global $CFG, $DB;
890         $remoteclient = get_mnet_remote_client();
892         $CFG->usesid = true;
894         // We don't want to output anything to the client machine
895         $start = ob_start();
897         // We'll get session records in batches of 30
898         $superArray = array_chunk($array, 30);
900         $returnString = '';
902         foreach($superArray as $subArray) {
903             $subArray = array_values($subArray);
904             $instring = "('".implode("', '",$subArray)."')";
905             $query = "select id, session_id, username from {mnet_session} where username in $instring";
906             $results = $DB->get_records_sql($query);
908             if ($results == false) {
909                 // We seem to have a username that breaks our query:
910                 // TODO: Handle this error appropriately
911                 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
912             } else {
913                 foreach($results as $emigrant) {
914                     session_touch($emigrant->session_id);
915                 }
916             }
917         }
919         $end = ob_end_clean();
921         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
922         return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
923     }
925     /**
926      * Cron function will be called automatically by cron.php every 5 minutes
927      *
928      * @return void
929      */
930     function cron() {
931         global $DB;
933         // run the keepalive client
934         $this->keepalive_client();
936         // admin/cron.php should have run srand for us
937         $random100 = rand(0,100);
938         if ($random100 < 10) {     // Approximately 10% of the time.
939             // nuke olden sessions
940             $longtime = time() - (1 * 3600 * 24);
941             $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
942         }
943     }
945     /**
946      * Cleanup any remote mnet_sessions, kill the local mnet_session data
947      *
948      * This is called by require_logout in moodlelib
949      *
950      * @return   void
951      */
952     function prelogout_hook() {
953         global $CFG, $USER;
955         if (!is_enabled_auth('mnet')) {
956             return;
957         }
959         // If the user is local to this Moodle:
960         if ($USER->mnethostid == $this->mnet->id) {
961             $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
963         // Else the user has hit 'logout' at a Service Provider Moodle:
964         } else {
965             $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
967         }
968     }
970     /**
971      * The SP uses this function to kill the session on the parent IdP
972      *
973      * @param   string  $username       Username for session to kill
974      * @param   string  $useragent      SHA1 hash of user agent to look for
975      * @return  string                  A plaintext report of what has happened
976      */
977     function kill_parent($username, $useragent) {
978         global $CFG, $USER, $DB;
980         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
981         $sql = "
982             select
983                 *
984             from
985                 {mnet_session} s
986             where
987                 s.username   = ? AND
988                 s.useragent  = ? AND
989                 s.mnethostid = ?";
991         $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
993         $ignore = $DB->delete_records('mnet_session',
994                                  array('username'=>$username,
995                                  'useragent'=>$useragent,
996                                  'mnethostid'=>$USER->mnethostid));
998         if (false != $mnetsessions) {
999             $mnet_peer = new mnet_peer();
1000             $mnet_peer->set_id($USER->mnethostid);
1002             $mnet_request = new mnet_xmlrpc_client();
1003             $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1005             // set $token and $useragent parameters
1006             $mnet_request->add_param($username);
1007             $mnet_request->add_param($useragent);
1008             if ($mnet_request->send($mnet_peer) === false) {
1009                 debugging(join("\n", $mnet_request->error));
1010                 return false;
1011             }
1012         }
1014         return true;
1015     }
1017     /**
1018      * The IdP uses this function to kill child sessions on other hosts
1019      *
1020      * @param   string  $username       Username for session to kill
1021      * @param   string  $useragent      SHA1 hash of user agent to look for
1022      * @return  string                  A plaintext report of what has happened
1023      */
1024     function kill_children($username, $useragent) {
1025         global $CFG, $USER, $DB;
1026         $remoteclient = null;
1027         if (defined('MNET_SERVER')) {
1028             $remoteclient = get_mnet_remote_client();
1029         }
1030         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1032         $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1034         $returnstring = '';
1036         $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1038         if (false == $mnetsessions) {
1039             $returnstring .= "Could find no remote sessions\n";
1040             $mnetsessions = array();
1041         }
1043         foreach($mnetsessions as $mnetsession) {
1044             // If this script is being executed by a remote peer, that means the user has clicked
1045             // logout on that peer, and the session on that peer can be deleted natively.
1046             // Skip over it.
1047             if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1048                 continue;
1049             }
1050             $returnstring .=  "Deleting session\n";
1052             $mnet_peer = new mnet_peer();
1053             $mnet_peer->set_id($mnetsession->mnethostid);
1055             $mnet_request = new mnet_xmlrpc_client();
1056             $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1058             // set $token and $useragent parameters
1059             $mnet_request->add_param($username);
1060             $mnet_request->add_param($useragent);
1061             if ($mnet_request->send($mnet_peer) === false) {
1062                 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1063                           join("\n", $mnet_request->error));
1064             }
1065         }
1067         $ignore = $DB->delete_records('mnet_session',
1068                                  array('useragent'=>$useragent, 'userid'=>$userid));
1070         if (isset($remoteclient) && isset($remoteclient->id)) {
1071             session_kill_user($userid);
1072         }
1073         return $returnstring;
1074     }
1076     /**
1077      * When the IdP requests that child sessions are terminated,
1078      * this function will be called on each of the child hosts. The machine that
1079      * calls the function (over xmlrpc) provides us with the mnethostid we need.
1080      *
1081      * @param   string  $username       Username for session to kill
1082      * @param   string  $useragent      SHA1 hash of user agent to look for
1083      * @return  bool                    True on success
1084      */
1085     function kill_child($username, $useragent) {
1086         global $CFG, $DB;
1087         $remoteclient = get_mnet_remote_client();
1088         $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1089         $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1090         if (false != $session) {
1091             session_kill($session->session_id);
1092             return true;
1093         }
1094         return false;
1095     }
1097     /**
1098      * To delete a host, we must delete all current sessions that users from
1099      * that host are currently engaged in.
1100      *
1101      * @param   string  $sessionidarray   An array of session hashes
1102      * @return  bool                      True on success
1103      */
1104     function end_local_sessions(&$sessionArray) {
1105         global $CFG;
1106         if (is_array($sessionArray)) {
1107             while($session = array_pop($sessionArray)) {
1108                 session_kill($session->session_id);
1109             }
1110             return true;
1111         }
1112         return false;
1113     }
1115     /**
1116      * Returns the user's profile image info
1117      *
1118      * If the user exists and has a profile picture, the returned array will contain keys:
1119      *  f1          - the content of the default 100x100px image
1120      *  f1_mimetype - the mimetype of the f1 file
1121      *  f2          - the content of the 35x35px variant of the image
1122      *  f2_mimetype - the mimetype of the f2 file
1123      *
1124      * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1125      *
1126      * @see process_new_icon()
1127      * @uses mnet_remote_client callable via MNet XML-RPC
1128      * @param int $userid The id of the user
1129      * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1130      */
1131     function fetch_user_image($username) {
1132         global $CFG, $DB;
1134         if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
1135             $fs = get_file_storage();
1136             $usercontext = get_context_instance(CONTEXT_USER, $user->id, MUST_EXIST);
1137             $return = array();
1138             if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
1139                 $return['f1'] = base64_encode($f1->get_content());
1140                 $return['f1_mimetype'] = $f1->get_mimetype();
1141             } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
1142                 $return['f1'] = base64_encode($f1->get_content());
1143                 $return['f1_mimetype'] = $f1->get_mimetype();
1144             }
1145             if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
1146                 $return['f2'] = base64_encode($f2->get_content());
1147                 $return['f2_mimetype'] = $f2->get_mimetype();
1148             } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
1149                 $return['f2'] = base64_encode($f2->get_content());
1150                 $return['f2_mimetype'] = $f2->get_mimetype();
1151             }
1152             return $return;
1153         }
1154         return false;
1155     }
1157     /**
1158      * Returns the theme information and logo url as strings.
1159      *
1160      * @return string     The theme info
1161      */
1162     function fetch_theme_info() {
1163         global $CFG;
1165         $themename = "$CFG->theme";
1166         $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1168         $return['themename'] = $themename;
1169         $return['logourl'] = $logourl;
1170         return $return;
1171     }
1173     /**
1174      * Determines if an MNET host is providing the nominated service.
1175      *
1176      * @param int    $mnethostid   The id of the remote host
1177      * @param string $servicename  The name of the service
1178      * @return bool                Whether the service is available on the remote host
1179      */
1180     function has_service($mnethostid, $servicename) {
1181         global $CFG, $DB;
1183         $sql = "
1184             SELECT
1185                 svc.id as serviceid,
1186                 svc.name,
1187                 svc.description,
1188                 svc.offer,
1189                 svc.apiversion,
1190                 h2s.id as h2s_id
1191             FROM
1192                 {mnet_host} h,
1193                 {mnet_service} svc,
1194                 {mnet_host2service} h2s
1195             WHERE
1196                 h.deleted = '0' AND
1197                 h.id = h2s.hostid AND
1198                 h2s.hostid = ? AND
1199                 h2s.serviceid = svc.id AND
1200                 svc.name = ? AND
1201                 h2s.subscribe = '1'";
1203         return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1204     }
1206     /**
1207      * Checks the MNET access control table to see if the username/mnethost
1208      * is permitted to login to this moodle.
1209      *
1210      * @param string $username   The username
1211      * @param int    $mnethostid The id of the remote mnethost
1212      * @return bool              Whether the user can login from the remote host
1213      */
1214     function can_login_remotely($username, $mnethostid) {
1215         global $DB;
1217         $accessctrl = 'allow';
1218         $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1219         if (!empty($aclrecord)) {
1220             $accessctrl = $aclrecord->accessctrl;
1221         }
1222         return $accessctrl == 'allow';
1223     }
1225     function logoutpage_hook() {
1226         global $USER, $CFG, $redirect, $DB;
1228         if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1229             $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1230             $redirect = $host->wwwroot.'/';
1231         }
1232     }
1234     /**
1235      * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1236      *
1237      * @param object $logline The log information to be trimmed
1238      * @return object The passed logline object trimmed to not exceed storable limits
1239      */
1240     function trim_logline ($logline) {
1241         $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1242                         'url' => 255);
1243         foreach ($limits as $property => $limit) {
1244             if (isset($logline->$property)) {
1245                 $logline->$property = substr($logline->$property, 0, $limit);
1246             }
1247         }
1249         return $logline;
1250     }
1252     /**
1253      * Returns a list of potential IdPs that this authentication plugin supports.
1254      * This is used to provide links on the login page.
1255      *
1256      * @param string $wantsurl the relative url fragment the user wants to get to.  You can use this to compose a returnurl, for example
1257      *
1258      * @return array like:
1259      *              array(
1260      *                  array(
1261      *                      'url' => 'http://someurl',
1262      *                      'icon' => new pix_icon(...),
1263      *                      'name' => get_string('somename', 'auth_yourplugin'),
1264      *                 ),
1265      *             )
1266      */
1267     function loginpage_idp_list($wantsurl) {
1268         global $DB, $CFG;
1270         // strip off wwwroot, since the remote site will prefix it's return url with this
1271         $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1273         $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1274                   FROM {mnet_host} h
1275                   JOIN {mnet_host2service} m ON h.id = m.hostid
1276                   JOIN {mnet_service} s ON s.id = m.serviceid
1277                   JOIN {mnet_application} a ON h.applicationid = a.id
1278                  WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1279         $params = array('sso_sp', 0, 1);
1281         if (!empty($CFG->mnet_all_hosts_id)) {
1282             $sql .= " AND h.id <> ?";
1283             $params[] = $CFG->mnet_all_hosts_id;
1284         }
1286         if (!$hosts = $DB->get_records_sql($sql, $params)) {
1287             return array();
1288         }
1290         $idps = array();
1291         foreach ($hosts as $host) {
1292             $idps[] = array(
1293                 'url'  => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1294                 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1295                 'name' => $host->name,
1296             );
1297         }
1298         return $idps;
1299     }