auth/mnet - MDL-16872 Fix incorrect use of clone() on arrays
[moodle.git] / auth / mnet / auth.php
1 <?php // $Id$
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     }
34     /**
35      * Provides the allowed RPC services from this class as an array.
36      * @return array  Allowed RPC services.
37      */
38     function mnet_publishes() {
40         $sso_idp = array();
41         $sso_idp['name']        = 'sso_idp'; // Name & Description go in lang file
42         $sso_idp['apiversion']  = 1;
43         $sso_idp['methods']     = array('user_authorise','keepalive_server', 'kill_children',
44                                         'refresh_log', 'fetch_user_image', 'fetch_theme_info',
45                                         'update_enrolments');
47         $sso_sp = array();
48         $sso_sp['name']         = 'sso_sp'; // Name & Description go in lang file
49         $sso_sp['apiversion']   = 1;
50         $sso_sp['methods']      = array('keepalive_client','kill_child');
52         return array($sso_idp, $sso_sp);
53     }
55     /**
56      * This function is normally used to determine if the username and password
57      * are correct for local logins. Always returns false, as local users do not
58      * need to login over mnet xmlrpc.
59      *
60      * @param string $username The username
61      * @param string $password The password
62      * @return bool Authentication success or failure.
63      */
64     function user_login($username, $password) {
65         return false; // print_error("mnetlocal");
66     }
68     /**
69      * Return user data for the provided token, compare with user_agent string.
70      *
71      * @param  string $token    The unique ID provided by remotehost.
72      * @param  string $UA       User Agent string.
73      * @return array  $userdata Array of user info for remote host
74      */
75     function user_authorise($token, $useragent) {
76         global $CFG, $MNET, $SITE, $MNET_REMOTE_CLIENT, $DB;
77         require_once $CFG->dirroot . '/mnet/xmlrpc/server.php';
79         $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent));
80         if (empty($mnet_session)) {
81             echo mnet_server_fault(1, get_string('authfail_nosessionexists', 'mnet'));
82             exit;
83         }
85         // check session confirm timeout
86         if ($mnet_session->confirm_timeout < time()) {
87             echo mnet_server_fault(2, get_string('authfail_sessiontimedout', 'mnet'));
88             exit;
89         }
91         // session okay, try getting the user
92         if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid))) {
93             echo mnet_server_fault(3, get_string('authfail_usermismatch', 'mnet'));
94             exit;
95         }
97         $userdata = array();
98         $userdata['username']                = $user->username;
99         $userdata['email']                   = $user->email;
100         $userdata['auth']                    = 'mnet';
101         $userdata['confirmed']               = $user->confirmed;
102         $userdata['deleted']                 = $user->deleted;
103         $userdata['firstname']               = $user->firstname;
104         $userdata['lastname']                = $user->lastname;
105         $userdata['city']                    = $user->city;
106         $userdata['country']                 = $user->country;
107         $userdata['lang']                    = $user->lang;
108         $userdata['timezone']                = $user->timezone;
109         $userdata['description']             = $user->description;
110         $userdata['mailformat']              = $user->mailformat;
111         $userdata['maildigest']              = $user->maildigest;
112         $userdata['maildisplay']             = $user->maildisplay;
113         $userdata['htmleditor']              = $user->htmleditor;
114         $userdata['wwwroot']                 = $MNET->wwwroot;
115         $userdata['session.gc_maxlifetime']  = ini_get('session.gc_maxlifetime');
116         $userdata['picture']                 = $user->picture;
117         if (!empty($user->picture)) {
118             $imagefile = make_user_directory($user->id, true) . "/f1.jpg";
119             if (file_exists($imagefile)) {
120                 $userdata['imagehash'] = sha1(file_get_contents($imagefile));
121             }
122         }
124         $userdata['myhosts'] = array();
125         if($courses = get_my_courses($user->id, 'id', 'id, visible')) {
126             $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
127         }
129         $sql = "
130                 SELECT
131                     h.name as hostname,
132                     h.wwwroot,
133                     h.id as hostid,
134                     count(c.id) as count
135                 FROM
136                     {mnet_enrol_course} c,
137                     {mnet_enrol_assignments} a,
138                     {mnet_host} h
139                 WHERE
140                     c.id      =  a.courseid   AND
141                     c.hostid  =  h.id         AND
142                     a.userid  = ? AND
143                     c.hostid != ?
144                 GROUP BY
145                     h.name,
146                     h.id,
147                     h.wwwroot";
148         if ($courses = $DB->get_records_sql($sql, array($user->id, $MNET_REMOTE_CLIENT->id))) {
149             foreach($courses as $course) {
150                 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
151             }
152         }
154         return $userdata;
155     }
157     /**
158      * Generate a random string for use as an RPC session token.
159      */
160     function generate_token() {
161         return sha1(str_shuffle('' . mt_rand() . time()));
162     }
164     /**
165      * Starts an RPC jump session and returns the jump redirect URL.
166      *
167      * @param int $mnethostid id of the mnet host to jump to
168      * @param string $wantsurl url to redirect to after the jump (usually on remote system)
169      * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
170      *                                  rather than somewhere inside *its* wwwroot
171      */
172     function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
173         global $CFG, $USER, $MNET, $DB;
174         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
176         // check remote login permissions
177         if (! has_capability('moodle/site:mnetlogintoremote', get_context_instance(CONTEXT_SYSTEM))
178                 or is_mnet_remote_user($USER)
179                 or $USER->username == 'guest'
180                 or empty($USER->id)) {
181             print_error('notpermittedtojump', 'mnet');
182         }
184         // check for SSO publish permission first
185         if ($this->has_service($mnethostid, 'sso_sp') == false) {
186             print_error('hostnotconfiguredforsso', 'mnet');
187         }
189         // set RPC timeout to 30 seconds if not configured
190         // TODO: Is this needed/useful/problematic?
191         if (empty($this->config->rpc_negotiation_timeout)) {
192             set_config('rpc_negotiation_timeout', '30', 'auth/mnet');
193         }
195         // get the host info
196         $mnet_peer = new mnet_peer();
197         $mnet_peer->set_id($mnethostid);
199         // set up the session
200         $mnet_session = $DB->get_record('mnet_session',
201                                    array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
202                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
203         if ($mnet_session == false) {
204             $mnet_session = new object();
205             $mnet_session->mnethostid = $mnethostid;
206             $mnet_session->userid = $USER->id;
207             $mnet_session->username = $USER->username;
208             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
209             $mnet_session->token = $this->generate_token();
210             $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
211             $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
212             $mnet_session->session_id = session_id();
213             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
214         } else {
215             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
216             $mnet_session->token = $this->generate_token();
217             $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
218             $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
219             $mnet_session->session_id = session_id();
220             $DB->update_record('mnet_session', $mnet_session);
221         }
223         // construct the redirection URL
224         //$transport = mnet_get_protocol($mnet_peer->transport);
225         $wantsurl = urlencode($wantsurl);
226         $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$MNET->wwwroot}&wantsurl={$wantsurl}";
227         if ($wantsurlbackhere) {
228             $url .= '&remoteurl=1';
229         }
231         return $url;
232     }
234     /**
235      * This function confirms the remote (ID provider) host's mnet session
236      * by communicating the token and UA over the XMLRPC transport layer, and
237      * returns the local user record on success.
238      *
239      *   @param string $token           The random session token.
240      *   @param string $remotewwwroot   The ID provider wwwroot.
241      *   @return array The local user record.
242      */
243     function confirm_mnet_session($token, $remotewwwroot) {
244         global $CFG, $MNET, $SESSION, $DB;
245         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
247         // verify the remote host is configured locally before attempting RPC call
248         if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotewwwroot, 'deleted' => 0))) {
249             print_error('notpermittedtoland', 'mnet');
250         }
252         // get the originating (ID provider) host info
253         $remotepeer = new mnet_peer();
254         $remotepeer->set_wwwroot($remotewwwroot);
256         // set up the RPC request
257         $mnetrequest = new mnet_xmlrpc_client();
258         $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
260         // set $token and $useragent parameters
261         $mnetrequest->add_param($token);
262         $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
264         // Thunderbirds are go! Do RPC call and store response
265         if ($mnetrequest->send($remotepeer) === true) {
266             $remoteuser = (object) $mnetrequest->response;
267         } else {
268             foreach ($mnetrequest->error as $errormessage) {
269                 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
270                 if($code == 702) {
271                     $site = get_site();
272                     print_error('mnet_session_prohibited', 'mnet', $remotewwwroot, format_string($site->fullname));
273                     exit;
274                 }
275                 $message .= "ERROR $code:<br/>$errormessage<br/>";
276             }
277             print_error("rpcerror", '', '', $message);
278         }
279         unset($mnetrequest);
281         if (empty($remoteuser) or empty($remoteuser->username)) {
282             print_error('unknownerror', 'mnet');
283             exit;
284         }
286         $firsttime = false;
288         // get the local record for the remote user
289         $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
291         // add the remote user to the database if necessary, and if allowed
292         // TODO: refactor into a separate function
293         if (empty($localuser) || ! $localuser->id) {
294             if (empty($this->config->auto_add_remote_users)) {
295                 print_error('nolocaluser', 'mnet');
296             }
297             $remoteuser->mnethostid = $remotehost->id;
298             $DB->insert_record('user', $remoteuser);
299             $firsttime = true;
300             if (! $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id))) {
301                 print_error('nolocaluser', 'mnet');
302             }
303         }
305         // check sso access control list for permission first
306         if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
307             print_error('sso_mnet_login_refused', 'mnet', '', array($localuser->username, $remotehost->name));
308         }
310         $session_gc_maxlifetime = 1440;
312         // update the local user record with remote user data
313         foreach ((array) $remoteuser as $key => $val) {
314             if ($key == 'session.gc_maxlifetime') {
315                 $session_gc_maxlifetime = $val;
316                 continue;
317             }
319             // TODO: fetch image if it has changed
320             if ($key == 'imagehash') {
321                 $dirname = make_user_directory($localuser->id, true);
322                 $filename = "$dirname/f1.jpg";
324                 $localhash = '';
325                 if (file_exists($filename)) {
326                     $localhash = sha1(file_get_contents($filename));
327                 } elseif (!file_exists($dirname)) {
328                     mkdir($dirname);
329                 }
331                 if ($localhash != $val) {
332                     // fetch image from remote host
333                     $fetchrequest = new mnet_xmlrpc_client();
334                     $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
335                     $fetchrequest->add_param($localuser->username);
336                     if ($fetchrequest->send($remotepeer) === true) {
337                         if (strlen($fetchrequest->response['f1']) > 0) {
338                             $imagecontents = base64_decode($fetchrequest->response['f1']);
339                             file_put_contents($filename, $imagecontents);
340                             $localuser->picture = 1;
341                         }
342                         if (strlen($fetchrequest->response['f2']) > 0) {
343                             $imagecontents = base64_decode($fetchrequest->response['f2']);
344                             file_put_contents($dirname.'/f2.jpg', $imagecontents);
345                         }
346                     }
347                 }
348             }
350             if($key == 'myhosts') {
351                 $localuser->mnet_foreign_host_array = array();
352                 foreach($val as $rhost) {
353                     $name  = clean_param($rhost['name'], PARAM_ALPHANUM);
354                     $url   = clean_param($rhost['url'], PARAM_URL);
355                     $count = clean_param($rhost['count'], PARAM_INT);
356                     $url_is_local = stristr($url , $CFG->wwwroot);
357                     if (!empty($name) && !empty($count) && empty($url_is_local)) {
358                         $localuser->mnet_foreign_host_array[] = array('name'  => $name,
359                                                                       'url'   => $url,
360                                                                       'count' => $count);
361                     }
362                 }
363             }
365             $localuser->{$key} = $val;
366         }
368         $localuser->mnethostid = $remotepeer->id;
370         $DB->update_record('user', $localuser);
372         // set up the session
373         $mnet_session = $DB->get_record('mnet_session',
374                                    array('userid'=>$localuser->id, 'mnethostid'=>$remotepeer->id,
375                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
376         if ($mnet_session == false) {
377             $mnet_session = new object();
378             $mnet_session->mnethostid = $remotepeer->id;
379             $mnet_session->userid = $localuser->id;
380             $mnet_session->username = $localuser->username;
381             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
382             $mnet_session->token = $token; // Needed to support simultaneous sessions
383                                            // and preserving DB rec uniqueness
384             $mnet_session->confirm_timeout = time();
385             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
386             $mnet_session->session_id = session_id();
387             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
388         } else {
389             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
390             $DB->update_record('mnet_session', $mnet_session);
391         }
393         if (!$firsttime) {
394             // repeat customer! let the IDP know about enrolments
395             // we have for this user.
396             // set up the RPC request
397             $mnetrequest = new mnet_xmlrpc_client();
398             $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
400             // pass username and an assoc array of "my courses"
401             // with info so that the IDP can maintain mnet_enrol_assignments
402             $mnetrequest->add_param($remoteuser->username);
403             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary,
404                        startdate, cost, currency, defaultrole, visible';
405             $courses = get_my_courses($localuser->id, 'visible DESC,sortorder ASC', $fields);
406             if (is_array($courses) && !empty($courses)) {
407                 // Second request to do the JOINs that we'd have done
408                 // inside get_my_courses() if we had been allowed
409                 $sql = "SELECT c.id,
410                                cc.name AS cat_name, cc.description AS cat_description,
411                                r.shortname as defaultrolename
412                           FROM {course} c
413                           JOIN {course_categories} cc ON c.category = cc.id
414                           LEFT OUTER JOIN {role} r  ON c.defaultrole = r.id
415                          WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
416                 $extra = $DB->get_records_sql($sql);
418                 $keys = array_keys($courses);
419                 $defaultrolename = $DB->get_field('role', 'shortname', array('id'=>$CFG->defaultcourseroleid));
420                 foreach ($keys AS $id) {
421                     if ($courses[$id]->visible == 0) {
422                         unset($courses[$id]);
423                         continue;
424                     }
425                     $courses[$id]->cat_id          = $courses[$id]->category;
426                     $courses[$id]->defaultroleid   = $courses[$id]->defaultrole;
427                     unset($courses[$id]->category);
428                     unset($courses[$id]->defaultrole);
429                     unset($courses[$id]->visible);
431                     $courses[$id]->cat_name        = $extra[$id]->cat_name;
432                     $courses[$id]->cat_description = $extra[$id]->cat_description;
433                     if (!empty($extra[$id]->defaultrolename)) {
434                         $courses[$id]->defaultrolename = $extra[$id]->defaultrolename;
435                     } else {
436                         $courses[$id]->defaultrolename = $defaultrolename;
437                     }
438                     // coerce to array
439                     $courses[$id] = (array)$courses[$id];
440                 }
441             } else {
442                 // if the array is empty, send it anyway
443                 // we may be clearing out stale entries
444                 $courses = array();
445             }
446             $mnetrequest->add_param($courses);
448             // Call 0800-RPC Now! -- we don't care too much if it fails
449             // as it's just informational.
450             if ($mnetrequest->send($remotepeer) === false) {
451                 // error_log(print_r($mnetrequest->error,1));
452             }
453         }
455         return $localuser;
456     }
458     /**
459      * Invoke this function _on_ the IDP to update it with enrolment info local to
460      * the SP right after calling user_authorise()
461      *
462      * Normally called by the SP after calling
463      *
464      *   @param string $username        The username
465      *   @param string $courses         Assoc array of courses following the structure of mnet_enrol_course
466      *   @return bool
467      */
468     function update_enrolments($username, $courses) {
469         global $MNET_REMOTE_CLIENT, $CFG, $DB;
471         if (empty($username) || !is_array($courses)) {
472             return false;
473         }
474         // make sure it is a user we have an in active session
475         // with that host...
476         $userid = $DB->get_field('mnet_session', 'userid',
477                             array('username'=>$username, 'mnethostid'=>$MNET_REMOTE_CLIENT->id));
478         if (!$userid) {
479             return false;
480         }
482         if (empty($courses)) { // no courses? clear out quickly
483             $DB->delete_records('mnet_enrol_assignments', array('hostid'=>$MNET_REMOTE_CLIENT->id, 'userid'=>$userid));
484             return true;
485         }
487         // IMPORTANT: Ask for remoteid as the first element in the query, so
488         // that the array that comes back is indexed on the same field as the
489         // array that we have received from the remote client
490         $sql = '
491                 SELECT
492                     c.remoteid,
493                     c.id,
494                     c.cat_id,
495                     c.cat_name,
496                     c.cat_description,
497                     c.sortorder,
498                     c.fullname,
499                     c.shortname,
500                     c.idnumber,
501                     c.summary,
502                     c.startdate,
503                     c.cost,
504                     c.currency,
505                     c.defaultroleid,
506                     c.defaultrolename,
507                     a.id as assignmentid
508                 FROM
509                     {mnet_enrol_course} c
510                 LEFT JOIN {mnet_enrol_assignments} a
511                 ON
512                    (a.courseid = c.id AND
513                     a.hostid   = c.hostid AND
514                     a.userid = ?)
515                 WHERE
516                     c.hostid = ?';
518         $currentcourses = $DB->get_records_sql($sql, array($userid, $MNET_REMOTE_CLIENT->id));
520         $local_courseid_array = array();
521         foreach($courses as $course) {
523             $course['remoteid'] = $course['id'];
524             $course['hostid']   =  (int)$MNET_REMOTE_CLIENT->id;
525             $userisregd         = false;
527             // First up - do we have a record for this course?
528             if (!array_key_exists($course['remoteid'], $currentcourses)) {
529                 // No record - we must create it
530                 $course['id']  =  $DB->insert_record('mnet_enrol_course', (object)$course);
531                 $currentcourse = (object)$course;
532             } else {
533                 // Pointer to current course:
534                 $currentcourse =& $currentcourses[$course['remoteid']];
535                 // We have a record - is it up-to-date?
536                 $course['id'] = $currentcourse->id;
538                 $saveflag = false;
540                 foreach($course as $key => $value) {
541                     if ($currentcourse->$key != $value) {
542                         $saveflag = true;
543                         $currentcourse->$key = $value;
544                     }
545                 }
547                 if ($saveflag) {
548                     $DB->update_record('mnet_enrol_course', $currentcourse);
549                 }
551                 if (isset($currentcourse->assignmentid) && is_numeric($currentcourse->assignmentid)) {
552                     $userisregd = true;
553                 }
554             }
556             // By this point, we should always have a $dataObj->id
557             $local_courseid_array[] = $course['id'];
559             // Do we have a record for this assignment?
560             if ($userisregd) {
561                 // Yes - we know about this one already
562                 // We don't want to do updates because the new data is probably
563                 // 'less complete' than the data we have.
564             } else {
565                 // No - create a record
566                 $assignObj = new stdClass();
567                 $assignObj->userid    = $userid;
568                 $assignObj->hostid    = (int)$MNET_REMOTE_CLIENT->id;
569                 $assignObj->courseid  = $course['id'];
570                 $assignObj->rolename  = $course['defaultrolename'];
571                 $assignObj->id = $DB->insert_record('mnet_enrol_assignments', $assignObj);
572             }
573         }
575         // Clean up courses that the user is no longer enrolled in.
576         $local_courseid_string = implode(', ', $local_courseid_array);
577         $whereclause = " userid = ? AND hostid = ? AND courseid NOT IN ($local_courseid_string)";
578         $DB->delete_records_select('mnet_enrol_assignments', $whereclause, array($userid, $MNET_REMOTE_CLIENT->id));
579     }
581     /**
582      * Returns true if this authentication plugin is 'internal'.
583      *
584      * @return bool
585      */
586     function is_internal() {
587         return false;
588     }
590     /**
591      * Returns true if this authentication plugin can change the user's
592      * password.
593      *
594      * @return bool
595      */
596     function can_change_password() {
597         //TODO: it should be able to redirect, right?
598         return false;
599     }
601     /**
602      * Returns the URL for changing the user's pw, or false if the default can
603      * be used.
604      *
605      * @return string
606      */
607     function change_password_url() {
608         return '';
609     }
611     /**
612      * Prints a form for configuring this authentication plugin.
613      *
614      * This function is called from admin/auth.php, and outputs a full page with
615      * a form for configuring this plugin.
616      *
617      * @param array $page An object containing all the data for this page.
618      */
619     function config_form($config, $err, $user_fields) {
620         global $CFG, $DB;
622          $query = "
623             SELECT
624                 h.id,
625                 h.name as hostname,
626                 h.wwwroot,
627                 h2idp.publish as idppublish,
628                 h2idp.subscribe as idpsubscribe,
629                 idp.name as idpname,
630                 h2sp.publish as sppublish,
631                 h2sp.subscribe as spsubscribe,
632                 sp.name as spname
633             FROM
634                 {mnet_host} h
635             LEFT JOIN
636                 {mnet_host2service} h2idp
637             ON
638                (h.id = h2idp.hostid AND
639                (h2idp.publish = 1 OR
640                 h2idp.subscribe = 1))
641             INNER JOIN
642                 {mnet_service} idp
643             ON
644                (h2idp.serviceid = idp.id AND
645                 idp.name = 'sso_idp')
646             LEFT JOIN
647                 {mnet_host2service} h2sp
648             ON
649                (h.id = h2sp.hostid AND
650                (h2sp.publish = 1 OR
651                 h2sp.subscribe = 1))
652             INNER JOIN
653                 {mnet_service} sp
654             ON
655                (h2sp.serviceid = sp.id AND
656                 sp.name = 'sso_sp')
657             WHERE
658                ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
659                (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
660                 h.id != ?
661             ORDER BY
662                 h.name ASC";
664         $id_providers       = array();
665         $service_providers  = array();
666         if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
667             foreach($resultset as $hostservice) {
668                 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
669                     $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
670                 }
671                 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
672                     $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
673                 }
674             }
675         }
677         include "config.html";
678     }
680     /**
681      * Processes and stores configuration data for this authentication plugin.
682      */
683     function process_config($config) {
684         // set to defaults if undefined
685         if (!isset ($config->rpc_negotiation_timeout)) {
686             $config->rpc_negotiation_timeout = '30';
687         }
688         if (!isset ($config->auto_add_remote_users)) {
689             $config->auto_add_remote_users = '0';
690         }
692         // save settings
693         set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth/mnet');
694         set_config('auto_add_remote_users',   $config->auto_add_remote_users,   'auth/mnet');
696         return true;
697     }
699     /**
700      * Poll the IdP server to let it know that a user it has authenticated is still
701      * online
702      *
703      * @return  void
704      */
705     function keepalive_client() {
706         global $CFG, $MNET, $DB;
707         $cutoff = time() - 300; // TODO - find out what the remote server's session
708                                 // cutoff is, and preempt that
710         $sql = "
711             select
712                 id,
713                 username,
714                 mnethostid
715             from
716                 {user}
717             where
718                 lastaccess > ? AND
719                 mnethostid != ?
720             order by
721                 mnethostid";
723         $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
725         if ($immigrants == false) {
726             return true;
727         }
729         $usersArray = array();
730         foreach($immigrants as $immigrant) {
731             $usersArray[$immigrant->mnethostid][] = $immigrant->username;
732         }
734         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
735         foreach($usersArray as $mnethostid => $users) {
736             $mnet_peer = new mnet_peer();
737             $mnet_peer->set_id($mnethostid);
739             $mnet_request = new mnet_xmlrpc_client();
740             $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
742             // set $token and $useragent parameters
743             $mnet_request->add_param($users);
745             if ($mnet_request->send($mnet_peer) === true) {
746                 if (!isset($mnet_request->response['code'])) {
747                     debugging("Server side error has occured on host $mnethostid");
748                     continue;
749                 } elseif ($mnet_request->response['code'] > 0) {
750                     debugging($mnet_request->response['message']);
751                 }
753                 if (!isset($mnet_request->response['last log id'])) {
754                     debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
755                     continue;
756                 }
757             } else {
758                 debugging("Server side error has occured on host $mnethostid: " .
759                           join("\n", $mnet_request->error));
760                 break;
761             }
762             $mnethostlogssql = "
763             SELECT
764                 mhostlogs.remoteid, mhostlogs.time, mhostlogs.userid, mhostlogs.ip,
765                 mhostlogs.course, mhostlogs.module, mhostlogs.cmid, mhostlogs.action,
766                 mhostlogs.url, mhostlogs.info, mhostlogs.username, c.fullname as coursename,
767                 c.modinfo
768             FROM
769                 (
770                     SELECT
771                         l.id as remoteid, l.time, l.userid, l.ip, l.course, l.module, l.cmid,
772                         l.action, l.url, l.info, u.username
773                     FROM
774                         {user} u
775                         INNER JOIN {log} l on l.userid = u.id
776                     WHERE
777                         u.mnethostid = ?
778                         AND l.id > ?
779                     ORDER BY remoteid ASC
780                     LIMIT 500
781                 ) mhostlogs
782                 INNER JOIN {course} c on c.id = mhostlogs.course
783             ORDER by mhostlogs.remoteid ASC";
785             $mnethostlogs = $DB->get_records_sql($mnethostlogssql, array($mnethostid, $mnet_request->response['last log id']));
787             if ($mnethostlogs == false) {
788                 continue;
789             }
791             $processedlogs = array();
793             foreach($mnethostlogs as $hostlog) {
794                 // Extract the name of the relevant module instance from the
795                 // course modinfo if possible.
796                 if (!empty($hostlog->modinfo) && !empty($hostlog->cmid)) {
797                     $modinfo = unserialize($hostlog->modinfo);
798                     unset($hostlog->modinfo);
799                     $modulearray = array();
800                     foreach($modinfo as $module) {
801                         $modulearray[$module->cm] = urldecode($module->name);
802                     }
803                     $hostlog->resource_name = $modulearray[$hostlog->cmid];
804                 } else {
805                     $hostlog->resource_name = '';
806                 }
808                 $processedlogs[] = array (
809                                     'remoteid'      => $hostlog->remoteid,
810                                     'time'          => $hostlog->time,
811                                     'userid'        => $hostlog->userid,
812                                     'ip'            => $hostlog->ip,
813                                     'course'        => $hostlog->course,
814                                     'coursename'    => $hostlog->coursename,
815                                     'module'        => $hostlog->module,
816                                     'cmid'          => $hostlog->cmid,
817                                     'action'        => $hostlog->action,
818                                     'url'           => $hostlog->url,
819                                     'info'          => $hostlog->info,
820                                     'resource_name' => $hostlog->resource_name,
821                                     'username'      => $hostlog->username
822                                  );
823             }
825             unset($hostlog);
827             $mnet_request = new mnet_xmlrpc_client();
828             $mnet_request->set_method('auth/mnet/auth.php/refresh_log');
830             // set $token and $useragent parameters
831             $mnet_request->add_param($processedlogs);
833             if ($mnet_request->send($mnet_peer) === true) {
834                 if ($mnet_request->response['code'] > 0) {
835                     debugging($mnet_request->response['message']);
836                 }
837             } else {
838                 debugging("Server side error has occured on host $mnet_peer->ip: " .join("\n", $mnet_request->error));
839             }
840         }
841     }
843     /**
844      * Receives an array of log entries from an SP and adds them to the mnet_log
845      * table
846      *
847      * @param   array   $array      An array of usernames
848      * @return  string              "All ok" or an error message
849      */
850     function refresh_log($array) {
851         global $CFG, $MNET_REMOTE_CLIENT, $DB;
853         // We don't want to output anything to the client machine
854         $start = ob_start();
856         $returnString = '';
857         $DB->begin_sql();
858         $useridarray = array();
860         foreach($array as $logEntry) {
861             $logEntryObj = (object)$logEntry;
862             $logEntryObj->hostid = $MNET_REMOTE_CLIENT->id;
864             if (isset($useridarray[$logEntryObj->username])) {
865                 $logEntryObj->userid = $useridarray[$logEntryObj->username];
866             } else {
867                 $logEntryObj->userid = $DB->get_field('user', 'id', array('username'=>$logEntryObj->username, 'mnethostid'=>(int)$logEntryObj->hostid));
868                 if ($logEntryObj->userid == false) {
869                     $logEntryObj->userid = 0;
870                 }
871                 $useridarray[$logEntryObj->username] = $logEntryObj->userid;
872             }
874             unset($logEntryObj->username);
876             $logEntryObj = $this->trim_logline($logEntryObj);
877             $insertok = $DB->insert_record('mnet_log', $logEntryObj, false);           
879             if ($insertok) {
880                 $MNET_REMOTE_CLIENT->last_log_id = $logEntryObj->remoteid;
881             } else {
882                 $returnString .= 'Record with id '.$logEntryObj->remoteid." failed to insert.\n";
883             }
884         }
885         $MNET_REMOTE_CLIENT->commit();
886         $DB->commit_sql();
888         $end = ob_end_clean();
890         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok');
891         return array('code' => 1, 'message' => $returnString);
892     }
894     /**
895      * Receives an array of usernames from a remote machine and prods their
896      * sessions to keep them alive
897      *
898      * @param   array   $array      An array of usernames
899      * @return  string              "All ok" or an error message
900      */
901     function keepalive_server($array) {
902         global $MNET_REMOTE_CLIENT, $CFG, $DB;
904         $CFG->usesid = true;
906         // We don't want to output anything to the client machine
907         $start = ob_start();
909         // We'll get session records in batches of 30
910         $superArray = array_chunk($array, 30);
912         $returnString = '';
914         foreach($superArray as $subArray) {
915             $subArray = array_values($subArray);
916             $instring = "('".implode("', '",$subArray)."')";
917             $query = "select id, session_id, username from {mnet_session} where username in $instring";
918             $results = $DB->get_records_sql($query);
920             if ($results == false) {
921                 // We seem to have a username that breaks our query:
922                 // TODO: Handle this error appropriately
923                 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
924             } else {
925                 // TODO: This process of killing and re-starting the session
926                 // will cause PHP to forget any custom session_set_save_handler
927                 // stuff. Subsequent attempts to prod existing sessions will
928                 // fail, because PHP will look in wherever the default place
929                 // may be (files?) and probably create a new session with the
930                 // right session ID in that location. If it doesn't have write-
931                 // access to that location, then it will fail... not sure how
932                 // apparent that will be.
933                 // There is no way to capture what the custom session handler
934                 // is and then reset it on each pass - I checked that out
935                 // already.
936                 $sesscache = $_SESSION;
937                 $sessidcache = session_id();
938                 session_write_close();
939                 unset($_SESSION);
941                 $uc = ini_get('session.use_cookies');
942                 ini_set('session.use_cookies', false);
943                 foreach($results as $emigrant) {
945                     unset($_SESSION);
946                     session_name('MoodleSession'.$CFG->sessioncookie);
947                     session_id($emigrant->session_id);
948                     session_start();
949                     session_write_close();
950                 }
952                 ini_set('session.use_cookies', $uc);
953                 session_name('MoodleSession'.$CFG->sessioncookie);
954                 session_id($sessidcache);
955                 session_start();
956                 $_SESSION = $sesscache;
957                 session_write_close();
958             }
959         }
961         $end = ob_end_clean();
963         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
964         return array('code' => 1, 'message' => $returnString, 'last log id' => $MNET_REMOTE_CLIENT->last_log_id);
965     }
967     /**
968      * Cron function will be called automatically by cron.php every 5 minutes
969      *
970      * @return void
971      */
972     function cron() {
973         global $DB;
975         // run the keepalive client
976         $this->keepalive_client();
978         // admin/cron.php should have run srand for us
979         $random100 = rand(0,100);
980         if ($random100 < 10) {     // Approximately 10% of the time.
981             // nuke olden sessions
982             $longtime = time() - (1 * 3600 * 24);
983             $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
984         }
985     }
987     /**
988      * Cleanup any remote mnet_sessions, kill the local mnet_session data
989      *
990      * This is called by require_logout in moodlelib
991      *
992      * @return   void
993      */
994     function prelogout_hook() {
995         global $MNET, $CFG, $USER;
996         if (!is_enabled_auth('mnet')) {
997             return;
998         }
1000         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1002         // If the user is local to this Moodle:
1003         if ($USER->mnethostid == $MNET->id) {
1004             $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1006         // Else the user has hit 'logout' at a Service Provider Moodle:
1007         } else {
1008             $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
1010         }
1011     }
1013     /**
1014      * The SP uses this function to kill the session on the parent IdP
1015      *
1016      * @param   string  $username       Username for session to kill
1017      * @param   string  $useragent      SHA1 hash of user agent to look for
1018      * @return  string                  A plaintext report of what has happened
1019      */
1020     function kill_parent($username, $useragent) {
1021         global $CFG, $USER, $DB;
1023         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1024         $sql = "
1025             select
1026                 *
1027             from
1028                 {mnet_session} s
1029             where
1030                 s.username   = ? AND
1031                 s.useragent  = ? AND
1032                 s.mnethostid = ?";
1034         $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
1036         $ignore = $DB->delete_records('mnet_session',
1037                                  array('username'=>$username,
1038                                  'useragent'=>$useragent,
1039                                  'mnethostid'=>$USER->mnethostid));
1041         if (false != $mnetsessions) {
1042             $mnet_peer = new mnet_peer();
1043             $mnet_peer->set_id($USER->mnethostid);
1045             $mnet_request = new mnet_xmlrpc_client();
1046             $mnet_request->set_method('auth/mnet/auth.php/kill_children');
1048             // set $token and $useragent parameters
1049             $mnet_request->add_param($username);
1050             $mnet_request->add_param($useragent);
1051             if ($mnet_request->send($mnet_peer) === false) {
1052                 debugging(join("\n", $mnet_request->error));
1053                 return false;
1054             }
1055         }
1057         $_SESSION = array();
1058         return true;
1059     }
1061     /**
1062      * The IdP uses this function to kill child sessions on other hosts
1063      *
1064      * @param   string  $username       Username for session to kill
1065      * @param   string  $useragent      SHA1 hash of user agent to look for
1066      * @return  string                  A plaintext report of what has happened
1067      */
1068     function kill_children($username, $useragent) {
1069         global $CFG, $USER, $MNET_REMOTE_CLIENT, $DB;
1070         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
1072         $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
1074         $returnstring = '';
1076         $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
1078         if (false == $mnetsessions) {
1079             $returnstring .= "Could find no remote sessions\n";
1080             $mnetsessions = array();
1081         }
1083         foreach($mnetsessions as $mnetsession) {
1084             // If this script is being executed by a remote peer, that means the user has clicked
1085             // logout on that peer, and the session on that peer can be deleted natively.
1086             // Skip over it.
1087             if (isset($MNET_REMOTE_CLIENT->id) && ($mnetsession->mnethostid == $MNET_REMOTE_CLIENT->id)) {
1088                 continue;
1089             }
1090             $returnstring .=  "Deleting session\n";
1092             $mnet_peer = new mnet_peer();
1093             $mnet_peer->set_id($mnetsession->mnethostid);
1095             $mnet_request = new mnet_xmlrpc_client();
1096             $mnet_request->set_method('auth/mnet/auth.php/kill_child');
1098             // set $token and $useragent parameters
1099             $mnet_request->add_param($username);
1100             $mnet_request->add_param($useragent);
1101             if ($mnet_request->send($mnet_peer) === false) {
1102                 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
1103                           join("\n", $mnet_request->error));
1104             }
1105         }
1107         $ignore = $DB->delete_records('mnet_session',
1108                                  array('useragent'=>$useragent, 'userid'=>$userid));
1110         if (isset($MNET_REMOTE_CLIENT) && isset($MNET_REMOTE_CLIENT->id)) {
1111             $start = ob_start();
1113             // Save current session and cookie-use status
1114             $cookieuse = ini_get('session.use_cookies');
1115             ini_set('session.use_cookies', false);
1116             $sesscache = $_SESSION;
1117             $sessidcache = session_id();
1119             // Replace existing mnet session with user session & unset
1120             session_write_close();
1121             unset($_SESSION);
1122             session_id($mnetsession->session_id);
1123             session_start();
1124             session_unregister("USER");
1125             session_unregister("SESSION");
1126             unset($_SESSION);
1127             $_SESSION = array();
1128             session_write_close();
1130             // Restore previous info
1131             ini_set('session.use_cookies', $cookieuse);
1132             session_name('MoodleSession'.$CFG->sessioncookie);
1133             session_id($sessidcache);
1134             session_start();
1135             $_SESSION = $sesscache;
1136             session_write_close();
1138             $end = ob_end_clean();
1139         } else {
1140             $_SESSION = array();
1141         }
1142         return $returnstring;
1143     }
1145     /**
1146      * TODO:Untested When the IdP requests that child sessions are terminated,
1147      * this function will be called on each of the child hosts. The machine that
1148      * calls the function (over xmlrpc) provides us with the mnethostid we need.
1149      *
1150      * @param   string  $username       Username for session to kill
1151      * @param   string  $useragent      SHA1 hash of user agent to look for
1152      * @return  bool                    True on success
1153      */
1154     function kill_child($username, $useragent) {
1155         global $CFG, $MNET_REMOTE_CLIENT, $DB;
1156         $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$MNET_REMOTE_CLIENT->id, 'useragent'=>$useragent));
1157         if (false != $session) {
1158             $start = ob_start();
1160             $uc = ini_get('session.use_cookies');
1161             ini_set('session.use_cookies', false);
1162             $sesscache = $_SESSION;
1163             $sessidcache = session_id();
1164             session_write_close();
1165             unset($_SESSION);
1168             session_id($session->session_id);
1169             session_start();
1170             session_unregister("USER");
1171             session_unregister("SESSION");
1172             unset($_SESSION);
1173             $_SESSION = array();
1174             session_write_close();
1177             ini_set('session.use_cookies', $uc);
1178             session_name('MoodleSession'.$CFG->sessioncookie);
1179             session_id($sessidcache);
1180             session_start();
1181             $_SESSION = $sesscache;
1182             session_write_close();
1184             $end = ob_end_clean();
1185             return true;
1186         }
1187         return false;
1188     }
1190     /**
1191      * To delete a host, we must delete all current sessions that users from
1192      * that host are currently engaged in.
1193      *
1194      * @param   string  $sessionidarray   An array of session hashes
1195      * @return  bool                      True on success
1196      */
1197     function end_local_sessions(&$sessionArray) {
1198         global $CFG;
1199         if (is_array($sessionArray)) {
1200             $start = ob_start();
1202             $uc = ini_get('session.use_cookies');
1203             ini_set('session.use_cookies', false);
1204             $sesscache = $_SESSION;
1205             $sessidcache = session_id();
1206             session_write_close();
1207             unset($_SESSION);
1209             while($session = array_pop($sessionArray)) {
1210                 session_id($session->session_id);
1211                 session_start();
1212                 session_unregister("USER");
1213                 session_unregister("SESSION");
1214                 unset($_SESSION);
1215                 $_SESSION = array();
1216                 session_write_close();
1217             }
1219             ini_set('session.use_cookies', $uc);
1220             session_name('MoodleSession'.$CFG->sessioncookie);
1221             session_id($sessidcache);
1222             session_start();
1223             $_SESSION = $sesscache;
1225             $end = ob_end_clean();
1226             return true;
1227         }
1228         return false;
1229     }
1231     /**
1232      * Returns the user's image as a base64 encoded string.
1233      *
1234      * @param int $userid The id of the user
1235      * @return string     The encoded image
1236      */
1237     function fetch_user_image($username) {
1238         global $CFG, $DB;
1240         if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
1241             $filename1 = make_user_directory($user->id, true) . "/f1.jpg";
1242             $filename2 = make_user_directory($user->id, true) . "/f2.jpg";
1243             $return = array();
1244             if (file_exists($filename1)) {
1245                 $return['f1'] = base64_encode(file_get_contents($filename1));
1246             }
1247             if (file_exists($filename2)) {
1248                 $return['f2'] = base64_encode(file_get_contents($filename2));
1249             }
1250             return $return;
1251         }
1252         return false;
1253     }
1255     /**
1256      * Returns the theme information and logo url as strings.
1257      *
1258      * @return string     The theme info
1259      */
1260     function fetch_theme_info() {
1261         global $CFG;
1263         $themename = "$CFG->theme";
1264         $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1266         $return['themename'] = $themename;
1267         $return['logourl'] = $logourl;
1268         return $return;
1269     }
1271     /**
1272      * Determines if an MNET host is providing the nominated service.
1273      *
1274      * @param int    $mnethostid   The id of the remote host
1275      * @param string $servicename  The name of the service
1276      * @return bool                Whether the service is available on the remote host
1277      */
1278     function has_service($mnethostid, $servicename) {
1279         global $CFG, $DB;
1281         $sql = "
1282             SELECT
1283                 svc.id as serviceid,
1284                 svc.name,
1285                 svc.description,
1286                 svc.offer,
1287                 svc.apiversion,
1288                 h2s.id as h2s_id
1289             FROM
1290                 {mnet_host} h,
1291                 {mnet_service} svc,
1292                 {mnet_host2service} h2s
1293             WHERE
1294                 h.deleted = '0' AND
1295                 h.id = h2s.hostid AND
1296                 h2s.hostid = ? AND
1297                 h2s.serviceid = svc.id AND
1298                 svc.name = ? AND
1299                 h2s.subscribe = '1'";
1301         return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1302     }
1304     /**
1305      * Checks the MNET access control table to see if the username/mnethost
1306      * is permitted to login to this moodle.
1307      *
1308      * @param string $username   The username
1309      * @param int    $mnethostid The id of the remote mnethost
1310      * @return bool              Whether the user can login from the remote host
1311      */
1312     function can_login_remotely($username, $mnethostid) {
1313         global $DB;
1315         $accessctrl = 'allow';
1316         $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1317         if (!empty($aclrecord)) {
1318             $accessctrl = $aclrecord->accessctrl;
1319         }
1320         return $accessctrl == 'allow';
1321     }
1323     function logoutpage_hook() {
1324         global $USER, $CFG, $redirect, $DB;
1326         if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1327             $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1328             $redirect = $host->wwwroot.'/';
1329         }
1330     }
1332     /**
1333      * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1334      *
1335      * @param object $logline The log information to be trimmed
1336      * @return object The passed logline object trimmed to not exceed storable limits
1337      */
1338     function trim_logline ($logline) {
1339         $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1340                         'url' => 255);
1341         foreach ($limits as $property => $limit) {
1342             if (isset($logline->$property)) {
1343                 $logline->$property = substr($logline->$property, 0, $limit);
1344             }
1345         }
1347         return $logline;
1348     }
1353 ?>