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