MDL-24425 mnet: fixed SQL DISTINCT from a table containing text field
[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             //TODO: rewrite to use new file storage
84             /*
85             $imagefile = make_user_directory($user->id, true) . "/f1.jpg";
86             if (file_exists($imagefile)) {
87                 $userdata['imagehash'] = sha1(file_get_contents($imagefile));
88             }
89             */
90         }
92         $userdata['myhosts'] = array();
93         if ($courses = enrol_get_users_courses($user->id, false)) {
94             $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
95         }
97         $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
98                        COUNT(c.id) AS count
99                   FROM {mnetservice_enrol_courses} c
100                   JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
101                   JOIN {mnet_host} h ON h.id = c.hostid
102                  WHERE e.userid = ? AND c.hostid = ?
103               GROUP BY h.name, h.wwwroot, h.id";
105         if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) {
106             foreach($courses as $course) {
107                 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
108             }
109         }
111         return $userdata;
112     }
114     /**
115      * Generate a random string for use as an RPC session token.
116      */
117     function generate_token() {
118         return sha1(str_shuffle('' . mt_rand() . time()));
119     }
121     /**
122      * Starts an RPC jump session and returns the jump redirect URL.
123      *
124      * @param int $mnethostid id of the mnet host to jump to
125      * @param string $wantsurl url to redirect to after the jump (usually on remote system)
126      * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
127      *                                  rather than somewhere inside *its* wwwroot
128      */
129     function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
130         global $CFG, $USER, $DB;
131         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
133         // check remote login permissions
134         if (! has_capability('moodle/site:mnetlogintoremote', get_system_context())
135                 or is_mnet_remote_user($USER)
136                 or isguestuser()
137                 or !isloggedin()) {
138             print_error('notpermittedtojump', 'mnet');
139         }
141         // check for SSO publish permission first
142         if ($this->has_service($mnethostid, 'sso_sp') == false) {
143             print_error('hostnotconfiguredforsso', 'mnet');
144         }
146         // set RPC timeout to 30 seconds if not configured
147         if (empty($this->config->rpc_negotiation_timeout)) {
148             $this->config->rpc_negotiation_timeout = 30;
149             set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
150         }
152         // get the host info
153         $mnet_peer = new mnet_peer();
154         $mnet_peer->set_id($mnethostid);
156         // set up the session
157         $mnet_session = $DB->get_record('mnet_session',
158                                    array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
159                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
160         if ($mnet_session == false) {
161             $mnet_session = new stdClass();
162             $mnet_session->mnethostid = $mnethostid;
163             $mnet_session->userid = $USER->id;
164             $mnet_session->username = $USER->username;
165             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
166             $mnet_session->token = $this->generate_token();
167             $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
168             $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
169             $mnet_session->session_id = session_id();
170             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
171         } else {
172             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
173             $mnet_session->token = $this->generate_token();
174             $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
175             $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
176             $mnet_session->session_id = session_id();
177             $DB->update_record('mnet_session', $mnet_session);
178         }
180         // construct the redirection URL
181         //$transport = mnet_get_protocol($mnet_peer->transport);
182         $wantsurl = urlencode($wantsurl);
183         $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
184         if ($wantsurlbackhere) {
185             $url .= '&remoteurl=1';
186         }
188         return $url;
189     }
191     /**
192      * This function confirms the remote (ID provider) host's mnet session
193      * by communicating the token and UA over the XMLRPC transport layer, and
194      * returns the local user record on success.
195      *
196      *   @param string    $token           The random session token.
197      *   @param mnet_peer $remotepeer   The ID provider mnet_peer object.
198      *   @return array The local user record.
199      */
200     function confirm_mnet_session($token, $remotepeer) {
201         global $CFG, $DB;
202         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
204         // verify the remote host is configured locally before attempting RPC call
205         if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
206             print_error('notpermittedtoland', 'mnet');
207         }
209         // set up the RPC request
210         $mnetrequest = new mnet_xmlrpc_client();
211         $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
213         // set $token and $useragent parameters
214         $mnetrequest->add_param($token);
215         $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
217         // Thunderbirds are go! Do RPC call and store response
218         if ($mnetrequest->send($remotepeer) === true) {
219             $remoteuser = (object) $mnetrequest->response;
220         } else {
221             foreach ($mnetrequest->error as $errormessage) {
222                 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
223                 if($code == 702) {
224                     $site = get_site();
225                     print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname));
226                     exit;
227                 }
228                 $message .= "ERROR $code:<br/>$errormessage<br/>";
229             }
230             print_error("rpcerror", '', '', $message);
231         }
232         unset($mnetrequest);
234         if (empty($remoteuser) or empty($remoteuser->username)) {
235             print_error('unknownerror', 'mnet');
236             exit;
237         }
239         if (user_not_fully_set_up($remoteuser)) {
240             print_error('notenoughidpinfo', 'mnet');
241             exit;
242         }
244         $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
246         $remoteuser->auth = 'mnet';
247         $remoteuser->wwwroot = $remotepeer->wwwroot;
249         // the user may roam from Moodle 1.x where lang has _utf8 suffix
250         // also, make sure that the lang is actually installed, otherwise set site default
251         if (isset($remoteuser->lang)) {
252             $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
253         }
254         if (empty($remoteuser->lang)) {
255             if (!empty($CFG->lang)) {
256                 $remoteuser->lang = $CFG->lang;
257             } else {
258                 $remoteuser->lang = 'en';
259             }
260         }
261         $firsttime = false;
263         // get the local record for the remote user
264         $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
266         // add the remote user to the database if necessary, and if allowed
267         // TODO: refactor into a separate function
268         if (empty($localuser) || ! $localuser->id) {
269             /*
270             if (empty($this->config->auto_add_remote_users)) {
271                 print_error('nolocaluser', 'mnet');
272             } See MDL-21327   for why this is commented out
273             */
274             $remoteuser->mnethostid = $remotehost->id;
275             $remoteuser->firstaccess = time(); // First time user in this server, grab it here
277             $remoteuser->id = $DB->insert_record('user', $remoteuser);
278             $firsttime = true;
279             $localuser = $remoteuser;
280         }
282         // check sso access control list for permission first
283         if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
284             print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
285         }
287         // update the local user record with remote user data
288         foreach ((array) $remoteuser as $key => $val) {
290             // TODO: fetch image if it has changed
291             //TODO: rewrite to use new file storage
292             if ($key == 'imagehash') {
293                 /*
294                 $dirname = make_user_directory($localuser->id, true);
295                 $filename = "$dirname/f1.jpg";
297                 $localhash = '';
298                 if (file_exists($filename)) {
299                     $localhash = sha1(file_get_contents($filename));
300                 } elseif (!file_exists($dirname)) {
301                     mkdir($dirname);
302                 }
304                 if ($localhash != $val) {
305                     // fetch image from remote host
306                     $fetchrequest = new mnet_xmlrpc_client();
307                     $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
308                     $fetchrequest->add_param($localuser->username);
309                     if ($fetchrequest->send($remotepeer) === true) {
310                         if (strlen($fetchrequest->response['f1']) > 0) {
311                             $imagecontents = base64_decode($fetchrequest->response['f1']);
312                             file_put_contents($filename, $imagecontents);
313                             $localuser->picture = 1;
314                         }
315                         if (strlen($fetchrequest->response['f2']) > 0) {
316                             $imagecontents = base64_decode($fetchrequest->response['f2']);
317                             file_put_contents($dirname.'/f2.jpg', $imagecontents);
318                         }
319                     }
320                 }
321                 */
322             }
324             if($key == 'myhosts') {
325                 $localuser->mnet_foreign_host_array = array();
326                 foreach($val as $rhost) {
327                     $name  = clean_param($rhost['name'], PARAM_ALPHANUM);
328                     $url   = clean_param($rhost['url'], PARAM_URL);
329                     $count = clean_param($rhost['count'], PARAM_INT);
330                     $url_is_local = stristr($url , $CFG->wwwroot);
331                     if (!empty($name) && !empty($count) && empty($url_is_local)) {
332                         $localuser->mnet_foreign_host_array[] = array('name'  => $name,
333                                                                       'url'   => $url,
334                                                                       'count' => $count);
335                     }
336                 }
337             }
339             $localuser->{$key} = $val;
340         }
342         $localuser->mnethostid = $remotepeer->id;
343         if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
344             $localuser->firstaccess = time();
345         }
347         $DB->update_record('user', $localuser);
349         if (!$firsttime) {
350             // repeat customer! let the IDP know about enrolments
351             // we have for this user.
352             // set up the RPC request
353             $mnetrequest = new mnet_xmlrpc_client();
354             $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
356             // pass username and an assoc array of "my courses"
357             // with info so that the IDP can maintain mnetservice_enrol_enrolments
358             $mnetrequest->add_param($remoteuser->username);
359             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
360             $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
361             if (is_array($courses) && !empty($courses)) {
362                 // Second request to do the JOINs that we'd have done
363                 // inside enrol_get_users_courses() if we had been allowed
364                 $sql = "SELECT c.id,
365                                cc.name AS cat_name, cc.description AS cat_description
366                           FROM {course} c
367                           JOIN {course_categories} cc ON c.category = cc.id
368                          WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
369                 $extra = $DB->get_records_sql($sql);
371                 $keys = array_keys($courses);
372                 $defaultrole = reset(get_archetype_roles('student'));
373                 //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
374                 foreach ($keys AS $id) {
375                     if ($courses[$id]->visible == 0) {
376                         unset($courses[$id]);
377                         continue;
378                     }
379                     $courses[$id]->cat_id          = $courses[$id]->category;
380                     $courses[$id]->defaultroleid   = $defaultrole->id;
381                     unset($courses[$id]->category);
382                     unset($courses[$id]->visible);
384                     $courses[$id]->cat_name        = $extra[$id]->cat_name;
385                     $courses[$id]->cat_description = $extra[$id]->cat_description;
386                     $courses[$id]->defaultrolename = $defaultrole->name;
387                     // coerce to array
388                     $courses[$id] = (array)$courses[$id];
389                 }
390             } else {
391                 // if the array is empty, send it anyway
392                 // we may be clearing out stale entries
393                 $courses = array();
394             }
395             $mnetrequest->add_param($courses);
397             // Call 0800-RPC Now! -- we don't care too much if it fails
398             // as it's just informational.
399             if ($mnetrequest->send($remotepeer) === false) {
400                 // error_log(print_r($mnetrequest->error,1));
401             }
402         }
404         return $localuser;
405     }
408     /**
409      * creates (or updates) the mnet session once
410      * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
411      *
412      * @param stdclass  $user the local user (must exist already
413      * @param string    $token the jump/land token
414      * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
415      */
416     public function update_mnet_session($user, $token, $remotepeer) {
417         global $DB;
418         $session_gc_maxlifetime = 1440;
419         if (isset($user->session_gc_maxlifetime)) {
420             $session_gc_maxlifetime = $user->session_gc_maxlifetime;
421         }
422         if (!$mnet_session = $DB->get_record('mnet_session',
423                                    array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
424                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
425             $mnet_session = new stdClass();
426             $mnet_session->mnethostid = $remotepeer->id;
427             $mnet_session->userid = $user->id;
428             $mnet_session->username = $user->username;
429             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
430             $mnet_session->token = $token; // Needed to support simultaneous sessions
431                                            // and preserving DB rec uniqueness
432             $mnet_session->confirm_timeout = time();
433             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
434             $mnet_session->session_id = session_id();
435             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
436         } else {
437             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
438             $DB->update_record('mnet_session', $mnet_session);
439         }
440     }
444     /**
445      * Invoke this function _on_ the IDP to update it with enrolment info local to
446      * the SP right after calling user_authorise()
447      *
448      * Normally called by the SP after calling user_authorise()
449      *
450      * @param string $username The username
451      * @param array $courses  Assoc array of courses following the structure of mnetservice_enrol_courses
452      * @return bool
453      */
454     function update_enrolments($username, $courses) {
455         global $CFG, $DB;
456         $remoteclient = get_mnet_remote_client();
458         if (empty($username) || !is_array($courses)) {
459             return false;
460         }
461         // make sure it is a user we have an in active session
462         // with that host...
463         if (!$userid = $DB->get_field('mnet_session', 'userid',
464                             array('username'=>$username, 'mnethostid'=>$remoteclient->id))) {
465             throw new mnet_server_exception(1, 'authfail_nosessionexists');
466         }
468         if (empty($courses)) { // no courses? clear out quickly
469             $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
470             return true;
471         }
473         // IMPORTANT: Ask for remoteid as the first element in the query, so
474         // that the array that comes back is indexed on the same field as the
475         // array that we have received from the remote client
476         $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
477                        c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
478                        e.id AS enrolmentid
479                   FROM {mnetservice_enrol_courses} c
480              LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
481                  WHERE e.userid = ? AND c.hostid = ?";
483         $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
485         $local_courseid_array = array();
486         foreach($courses as $ix => $course) {
488             $course['remoteid'] = $course['id'];
489             $course['hostid']   =  (int)$remoteclient->id;
490             $userisregd         = false;
492             // if we do not have the the information about the remote course, it is not available
493             // to us for remote enrolment - skip
494             if (array_key_exists($course['remoteid'], $currentcourses)) {
495                 // Pointer to current course:
496                 $currentcourse =& $currentcourses[$course['remoteid']];
497                 // We have a record - is it up-to-date?
498                 $course['id'] = $currentcourse->id;
500                 $saveflag = false;
502                 foreach($course as $key => $value) {
503                     if ($currentcourse->$key != $value) {
504                         $saveflag = true;
505                         $currentcourse->$key = $value;
506                     }
507                 }
509                 if ($saveflag) {
510                     $DB->update_record('mnetervice_enrol_courses', $currentcourse);
511                 }
513                 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
514                     $userisregd = true;
515                 }
516             } else {
517                 unset ($courses[$ix]);
518                 continue;
519             }
521             // By this point, we should always have a $dataObj->id
522             $local_courseid_array[] = $course['id'];
524             // Do we have a record for this assignment?
525             if ($userisregd) {
526                 // Yes - we know about this one already
527                 // We don't want to do updates because the new data is probably
528                 // 'less complete' than the data we have.
529             } else {
530                 // No - create a record
531                 $assignObj = new stdClass();
532                 $assignObj->userid    = $userid;
533                 $assignObj->hostid    = (int)$remoteclient->id;
534                 $assignObj->remotecourseid = $course['remoteid'];
535                 $assignObj->rolename  = $course['defaultrolename'];
536                 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
537             }
538         }
540         // Clean up courses that the user is no longer enrolled in.
541         $local_courseid_string = implode(', ', $local_courseid_array);
542         $whereclause = " userid = ? AND hostid = ? AND courseid NOT IN ($local_courseid_string)";
543         $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
544     }
546     function prevent_local_passwords() {
547         return true;
548     }
550     /**
551      * Returns true if this authentication plugin is 'internal'.
552      *
553      * @return bool
554      */
555     function is_internal() {
556         return false;
557     }
559     /**
560      * Returns true if this authentication plugin can change the user's
561      * password.
562      *
563      * @return bool
564      */
565     function can_change_password() {
566         //TODO: it should be able to redirect, right?
567         return false;
568     }
570     /**
571      * Returns the URL for changing the user's pw, or false if the default can
572      * be used.
573      *
574      * @return moodle_url
575      */
576     function change_password_url() {
577         return null;
578     }
580     /**
581      * Prints a form for configuring this authentication plugin.
582      *
583      * This function is called from admin/auth.php, and outputs a full page with
584      * a form for configuring this plugin.
585      *
586      * @param object $config
587      * @param object $err
588      * @param array $user_fields
589      */
590     function config_form($config, $err, $user_fields) {
591         global $CFG, $DB;
593          $query = "
594             SELECT
595                 h.id,
596                 h.name as hostname,
597                 h.wwwroot,
598                 h2idp.publish as idppublish,
599                 h2idp.subscribe as idpsubscribe,
600                 idp.name as idpname,
601                 h2sp.publish as sppublish,
602                 h2sp.subscribe as spsubscribe,
603                 sp.name as spname
604             FROM
605                 {mnet_host} h
606             LEFT JOIN
607                 {mnet_host2service} h2idp
608             ON
609                (h.id = h2idp.hostid AND
610                (h2idp.publish = 1 OR
611                 h2idp.subscribe = 1))
612             INNER JOIN
613                 {mnet_service} idp
614             ON
615                (h2idp.serviceid = idp.id AND
616                 idp.name = 'sso_idp')
617             LEFT JOIN
618                 {mnet_host2service} h2sp
619             ON
620                (h.id = h2sp.hostid AND
621                (h2sp.publish = 1 OR
622                 h2sp.subscribe = 1))
623             INNER JOIN
624                 {mnet_service} sp
625             ON
626                (h2sp.serviceid = sp.id AND
627                 sp.name = 'sso_sp')
628             WHERE
629                ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
630                (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
631                 h.id != ?
632             ORDER BY
633                 h.name ASC";
635         $id_providers       = array();
636         $service_providers  = array();
637         if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
638             foreach($resultset as $hostservice) {
639                 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
640                     $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
641                 }
642                 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
643                     $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
644                 }
645             }
646         }
648         include "config.html";
649     }
651     /**
652      * Processes and stores configuration data for this authentication plugin.
653      */
654     function process_config($config) {
655         // set to defaults if undefined
656         if (!isset ($config->rpc_negotiation_timeout)) {
657             $config->rpc_negotiation_timeout = '30';
658         }
659         /*
660         if (!isset ($config->auto_add_remote_users)) {
661             $config->auto_add_remote_users = '0';
662         } See MDL-21327   for why this is commented out
663         set_config('auto_add_remote_users',   $config->auto_add_remote_users,   'auth_mnet');
664         */
666         // save settings
667         set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
669         return true;
670     }
672     /**
673      * Poll the IdP server to let it know that a user it has authenticated is still
674      * online
675      *
676      * @return  void
677      */
678     function keepalive_client() {
679         global $CFG, $DB;
680         $cutoff = time() - 300; // TODO - find out what the remote server's session
681                                 // cutoff is, and preempt that
683         $sql = "
684             select
685                 id,
686                 username,
687                 mnethostid
688             from
689                 {user}
690             where
691                 lastaccess > ? AND
692                 mnethostid != ?
693             order by
694                 mnethostid";
696         $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
698         if ($immigrants == false) {
699             return true;
700         }
702         $usersArray = array();
703         foreach($immigrants as $immigrant) {
704             $usersArray[$immigrant->mnethostid][] = $immigrant->username;
705         }
707         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
708         foreach($usersArray as $mnethostid => $users) {
709             $mnet_peer = new mnet_peer();
710             $mnet_peer->set_id($mnethostid);
712             $mnet_request = new mnet_xmlrpc_client();
713             $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
715             // set $token and $useragent parameters
716             $mnet_request->add_param($users);
718             if ($mnet_request->send($mnet_peer) === true) {
719                 if (!isset($mnet_request->response['code'])) {
720                     debugging("Server side error has occured on host $mnethostid");
721                     continue;
722                 } elseif ($mnet_request->response['code'] > 0) {
723                     debugging($mnet_request->response['message']);
724                 }
726                 if (!isset($mnet_request->response['last log id'])) {
727                     debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
728                     continue;
729                 }
730             } else {
731                 debugging("Server side error has occured on host $mnethostid: " .
732                           join("\n", $mnet_request->error));
733                 break;
734             }
735             $mnethostlogssql = "
736             SELECT
737                 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
738                 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
739                 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
740                 c.modinfo
741             FROM
742                 (
743                     SELECT
744                         l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
745                         l.action, l.url, l.info, u.username
746                     FROM
747                         {user} u
748                         INNER JOIN {log} l on l.userid = u.id
749                     WHERE
750                         u.mnethostid = ?
751                         AND l.id > ?
752                     ORDER BY remoteid ASC
753                     LIMIT 500
754                 ) mhostlogs
755                 INNER JOIN {course} c on c.id = mhostlogs.course
756             ORDER by mhostlogs.remoteid ASC";
758             $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
760             if ($mnethostlogs == false) {
761                 continue;
762             }
764             $processedlogs = array();
766             foreach($mnethostlogs as $hostlog) {
767                 // Extract the name of the relevant module instance from the
768                 // course modinfo if possible.
769                 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
770                     $modinfo = unserialize($hostlog->modinfo);
771                     unset($hostlog->modinfo);
772                     $modulearray = array();
773                     foreach($modinfo as $module) {
774                         $modulearray[$module->cm] = $module->name;
775                     }
776                     $hostlog->resource_name = $modulearray[$hostlog->cmid];
777                 } else {
778                     $hostlog->resource_name = '';
779                 }
781                 $processedlogs[] = array (
782                                     'remoteid'      => $hostlog->remoteid,
783                                     'time'          => $hostlog->time,
784                                     'userid'        => $hostlog->userid,
785                                     'ip'            => $hostlog->ip,
786                                     'course'        => $hostlog->course,
787                                     'coursename'    => $hostlog->coursename,
788                                     'module'        => $hostlog->module,
789                                     'cmid'          => $hostlog->cmid,
790                                     'action'        => $hostlog->action,
791                                     'url'           => $hostlog->url,
792                                     'info'          => $hostlog->info,
793                                     'resource_name' => $hostlog->resource_name,
794                                     'username'      => $hostlog->username
795                                  );
796             }
798             unset($hostlog);
800             $mnet_request = new mnet_xmlrpc_client();
801             $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
803             // set $token and $useragent parameters
804             $mnet_request->add_param($processedlogs);
806             if ($mnet_request->send($mnet_peer) === true) {
807                 if ($mnet_request->response['code'] > 0) {
808                     debugging($mnet_request->response['message']);
809                 }
810             } else {
811                 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
812             }
813         }
814     }
816     /**
817      * Receives an array of log entries from an SP and adds them to the mnet_log
818      * table
819      *
820      * @param   array   $array      An array of usernames
821      * @return  string              "All ok" or an error message
822      */
823     function refresh_log($array) {
824         global $CFG, $DB;
825         $remoteclient = get_mnet_remote_client();
827         // We don't want to output anything to the client machine
828         $start = ob_start();
830         $returnString = '';
831         $transaction = $DB->start_delegated_transaction();
832         $useridarray = array();
834         foreach($array as $logEntry) {
835             $logEntryObj = (object)$logEntry;
836             $logEntryObj->hostid = $remoteclient->id;
838             if (isset($useridarray[$logEntryObj->username])) {
839                 $logEntryObj->userid = $useridarray[$logEntryObj->username];
840             } else {
841                 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
842                 if ($logEntryObj->userid == false) {
843                     $logEntryObj->userid = 0;
844                 }
845                 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
846             }
848             unset($logEntryObj->username);
850             $logEntryObj = $this->trim_logline($logEntryObj);
851             $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);
853             if ($insertok) {
854                 $remoteclient->last_log_id = $logEntryObj->remoteid;
855             } else {
856                 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
857             }
858         }
859         $remoteclient->commit();
860         $transaction->allow_commit();
862         $end = ob_end_clean();
864         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
865         return array('code' => 1, 'message' => $returnString);
866     }
868     /**
869      * Receives an array of usernames from a remote machine and prods their
870      * sessions to keep them alive
871      *
872      * @param   array   $array      An array of usernames
873      * @return  string              "All ok" or an error message
874      */
875     function keepalive_server($array) {
876         global $CFG, $DB;
877         $remoteclient = get_mnet_remote_client();
879         $CFG->usesid = true;
881         // We don't want to output anything to the client machine
882         $start = ob_start();
884         // We'll get session records in batches of 30
885         $superArray = array_chunk($array, 30);
887         $returnString = '';
889         foreach($superArray as $subArray) {
890             $subArray = array_values($subArray);
891             $instring = "('".implode("', '",$subArray)."')";
892             $query = "select id, session_id, username from {mnet_session} where username in $instring";
893             $results = $DB->get_records_sql($query);
895             if ($results == false) {
896                 // We seem to have a username that breaks our query:
897                 // TODO: Handle this error appropriately
898                 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
899             } else {
900                 foreach($results as $emigrant) {
901                     session_touch($emigrant->session_id);
902                 }
903             }
904         }
906         $end = ob_end_clean();
908         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
909         return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
910     }
912     /**
913      * Cron function will be called automatically by cron.php every 5 minutes
914      *
915      * @return void
916      */
917     function cron() {
918         global $DB;
920         // run the keepalive client
921         $this->keepalive_client();
923         // admin/cron.php should have run srand for us
924         $random100 = rand(0,100);
925         if ($random100 < 10) {     // Approximately 10% of the time.
926             // nuke olden sessions
927             $longtime = time() - (1 * 3600 * 24);
928             $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
929         }
930     }
932     /**
933      * Cleanup any remote mnet_sessions, kill the local mnet_session data
934      *
935      * This is called by require_logout in moodlelib
936      *
937      * @return   void
938      */
939     function prelogout_hook() {
940         global $CFG, $USER;
942         if (!is_enabled_auth('mnet')) {
943             return;
944         }
946         // If the user is local to this Moodle:
947         if ($USER->mnethostid == $this->mnet->id) {
948             $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
950         // Else the user has hit 'logout' at a Service Provider Moodle:
951         } else {
952             $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
954         }
955     }
957     /**
958      * The SP uses this function to kill the session on the parent IdP
959      *
960      * @param   string  $username       Username for session to kill
961      * @param   string  $useragent      SHA1 hash of user agent to look for
962      * @return  string                  A plaintext report of what has happened
963      */
964     function kill_parent($username, $useragent) {
965         global $CFG, $USER, $DB;
967         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
968         $sql = "
969             select
970                 *
971             from
972                 {mnet_session} s
973             where
974                 s.username   = ? AND
975                 s.useragent  = ? AND
976                 s.mnethostid = ?";
978         $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
980         $ignore = $DB->delete_records('mnet_session',
981                                  array('username'=>$username,
982                                  'useragent'=>$useragent,
983                                  'mnethostid'=>$USER->mnethostid));
985         if (false != $mnetsessions) {
986             $mnet_peer = new mnet_peer();
987             $mnet_peer->set_id($USER->mnethostid);
989             $mnet_request = new mnet_xmlrpc_client();
990             $mnet_request->set_method('auth/mnet/auth.php/kill_children');
992             // set $token and $useragent parameters
993             $mnet_request->add_param($username);
994             $mnet_request->add_param($useragent);
995             if ($mnet_request->send($mnet_peer) === false) {
996                 debugging(join("\n", $mnet_request->error));
997                 return false;
998             }
999         }
1001         return true;
1002     }
1004     /**
1005      * The IdP uses this function to kill child sessions on other hosts
1006      *
1007      * @param   string  $username       Username for session to kill
1008      * @param   string  $useragent      SHA1 hash of user agent to look for
1009      * @return  string                  A plaintext report of what has happened
1010      */
1011     function kill_children($username, $useragent) {
1012         global $CFG, $USER, $DB;
1013         $remoteclient = null;
1014         if (defined('MNET_SERVER')) {
1015             $remoteclient = get_mnet_remote_client();
1016         }
1017         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1019         $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1021         $returnstring = '';
1023         $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1025         if (false == $mnetsessions) {
1026             $returnstring .= "Could find no remote sessions\n";
1027             $mnetsessions = array();
1028         }
1030         foreach($mnetsessions as $mnetsession) {
1031             // If this script is being executed by a remote peer, that means the user has clicked
1032             // logout on that peer, and the session on that peer can be deleted natively.
1033             // Skip over it.
1034             if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
1035                 continue;
1036             }
1037             $returnstring .=  "Deleting session\n";
1039             $mnet_peer = new mnet_peer();
1040             $mnet_peer->set_id($mnetsession->mnethostid);
1042             $mnet_request = new mnet_xmlrpc_client();
1043             $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1045             // set $token and $useragent parameters
1046             $mnet_request->add_param($username);
1047             $mnet_request->add_param($useragent);
1048             if ($mnet_request->send($mnet_peer) === false) {
1049                 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1050                           join("\n", $mnet_request->error));
1051             }
1052         }
1054         $ignore = $DB->delete_records('mnet_session',
1055                                  array('useragent'=>$useragent, 'userid'=>$userid));
1057         if (isset($remoteclient) && isset($remoteclient->id)) {
1058             session_kill_user($userid);
1059         }
1060         return $returnstring;
1061     }
1063     /**
1064      * When the IdP requests that child sessions are terminated,
1065      * this function will be called on each of the child hosts. The machine that
1066      * calls the function (over xmlrpc) provides us with the mnethostid we need.
1067      *
1068      * @param   string  $username       Username for session to kill
1069      * @param   string  $useragent      SHA1 hash of user agent to look for
1070      * @return  bool                    True on success
1071      */
1072     function kill_child($username, $useragent) {
1073         global $CFG, $DB;
1074         $remoteclient = get_mnet_remote_client();
1075         $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1076         $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
1077         if (false != $session) {
1078             session_kill($session->session_id);
1079             return true;
1080         }
1081         return false;
1082     }
1084     /**
1085      * To delete a host, we must delete all current sessions that users from
1086      * that host are currently engaged in.
1087      *
1088      * @param   string  $sessionidarray   An array of session hashes
1089      * @return  bool                      True on success
1090      */
1091     function end_local_sessions(&$sessionArray) {
1092         global $CFG;
1093         if (is_array($sessionArray)) {
1094             while($session = array_pop($sessionArray)) {
1095                 session_kill($session->session_id);
1096             }
1097             return true;
1098         }
1099         return false;
1100     }
1102     /**
1103      * Returns the user's image as a base64 encoded string.
1104      *
1105      * @param int $userid The id of the user
1106      * @return string     The encoded image
1107      */
1108     function fetch_user_image($username) {
1109         global $CFG, $DB;
1111         //TODO: rewrite to use new file storage
1112         return false;
1113         /*
1114         if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
1115             $filename1 = make_user_directory($user->id, true) . "/f1.jpg";
1116             $filename2 = make_user_directory($user->id, true) . "/f2.jpg";
1117             $return = array();
1118             if (file_exists($filename1)) {
1119                 $return['f1'] = base64_encode(file_get_contents($filename1));
1120             }
1121             if (file_exists($filename2)) {
1122                 $return['f2'] = base64_encode(file_get_contents($filename2));
1123             }
1124             return $return;
1125         }
1126         return false;
1127         */
1128     }
1130     /**
1131      * Returns the theme information and logo url as strings.
1132      *
1133      * @return string     The theme info
1134      */
1135     function fetch_theme_info() {
1136         global $CFG;
1138         $themename = "$CFG->theme";
1139         $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1141         $return['themename'] = $themename;
1142         $return['logourl'] = $logourl;
1143         return $return;
1144     }
1146     /**
1147      * Determines if an MNET host is providing the nominated service.
1148      *
1149      * @param int    $mnethostid   The id of the remote host
1150      * @param string $servicename  The name of the service
1151      * @return bool                Whether the service is available on the remote host
1152      */
1153     function has_service($mnethostid, $servicename) {
1154         global $CFG, $DB;
1156         $sql = "
1157             SELECT
1158                 svc.id as serviceid,
1159                 svc.name,
1160                 svc.description,
1161                 svc.offer,
1162                 svc.apiversion,
1163                 h2s.id as h2s_id
1164             FROM
1165                 {mnet_host} h,
1166                 {mnet_service} svc,
1167                 {mnet_host2service} h2s
1168             WHERE
1169                 h.deleted = '0' AND
1170                 h.id = h2s.hostid AND
1171                 h2s.hostid = ? AND
1172                 h2s.serviceid = svc.id AND
1173                 svc.name = ? AND
1174                 h2s.subscribe = '1'";
1176         return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1177     }
1179     /**
1180      * Checks the MNET access control table to see if the username/mnethost
1181      * is permitted to login to this moodle.
1182      *
1183      * @param string $username   The username
1184      * @param int    $mnethostid The id of the remote mnethost
1185      * @return bool              Whether the user can login from the remote host
1186      */
1187     function can_login_remotely($username, $mnethostid) {
1188         global $DB;
1190         $accessctrl = 'allow';
1191         $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1192         if (!empty($aclrecord)) {
1193             $accessctrl = $aclrecord->accessctrl;
1194         }
1195         return $accessctrl == 'allow';
1196     }
1198     function logoutpage_hook() {
1199         global $USER, $CFG, $redirect, $DB;
1201         if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1202             $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1203             $redirect = $host->wwwroot.'/';
1204         }
1205     }
1207     /**
1208      * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1209      *
1210      * @param object $logline The log information to be trimmed
1211      * @return object The passed logline object trimmed to not exceed storable limits
1212      */
1213     function trim_logline ($logline) {
1214         $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1215                         'url' => 255);
1216         foreach ($limits as $property => $limit) {
1217             if (isset($logline->$property)) {
1218                 $logline->$property = substr($logline->$property, 0, $limit);
1219             }
1220         }
1222         return $logline;
1223     }
1225     /**
1226      * Returns a list of potential IdPs that this authentication plugin supports.
1227      * This is used to provide links on the login page.
1228      *
1229      * @param string $wantsurl the relative url fragment the user wants to get to.  You can use this to compose a returnurl, for example
1230      *
1231      * @return array like:
1232      *              array(
1233      *                  array(
1234      *                      'url' => 'http://someurl',
1235      *                      'icon' => new pix_icon(...),
1236      *                      'name' => get_string('somename', 'auth_yourplugin'),
1237      *                 ),
1238      *             )
1239      */
1240     function loginpage_idp_list($wantsurl) {
1241         global $DB, $CFG;
1242         // strip off wwwroot, since the remote site will prefix it's return url with this
1243         $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1244         if (!$hosts = $DB->get_records_sql('SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url,a.name as application
1245                 FROM {mnet_host} h
1246                 JOIN {mnet_host2service} m ON h.id=m.hostid
1247                 JOIN {mnet_service} s ON s.id=m.serviceid
1248                 JOIN {mnet_application} a ON h.applicationid = a.id
1249                 WHERE s.name=? AND h.deleted=? AND m.publish = ?',
1250                 array('sso_sp', 0, 1))) {
1251             return array();
1252         }
1253         $idps = array();
1254         foreach ($hosts as $host) {
1255             $idps[] = array(
1256                 'url'  => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1257                 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1258                 'name' => $host->name,
1259             );
1260         }
1261         return $idps;
1262     }