ef3d1fc89f52a4b003e052f02454a1989a08c134
[moodle.git] / auth / mnet / auth.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Authentication Plugin: Moodle Network Authentication
19  * Multiple host authentication support for Moodle Network.
20  *
21  * @package auth_mnet
22  * @author Martin Dougiamas
23  * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->libdir.'/authlib.php');
30 /**
31  * Moodle Network authentication plugin.
32  */
33 class auth_plugin_mnet extends auth_plugin_base {
35     /**
36      * Constructor.
37      */
38     function auth_plugin_mnet() {
39         $this->authtype = 'mnet';
40         $this->config = get_config('auth_mnet');
41         $this->mnet = get_mnet_environment();
42     }
44     /**
45      * This function is normally used to determine if the username and password
46      * are correct for local logins. Always returns false, as local users do not
47      * need to login over mnet xmlrpc.
48      *
49      * @param string $username The username
50      * @param string $password The password
51      * @return bool Authentication success or failure.
52      */
53     function user_login($username, $password) {
54         return false; // print_error("mnetlocal");
55     }
57     /**
58      * Return user data for the provided token, compare with user_agent string.
59      *
60      * @param  string $token    The unique ID provided by remotehost.
61      * @param  string $UA       User Agent string.
62      * @return array  $userdata Array of user info for remote host
63      */
64     function user_authorise($token, $useragent) {
65         global $CFG, $SITE, $DB;
66         $remoteclient = get_mnet_remote_client();
67         require_once $CFG->dirroot . '/mnet/xmlrpc/serverlib.php';
69         $mnet_session = $DB->get_record('mnet_session', array('token'=>$token, 'useragent'=>$useragent));
70         if (empty($mnet_session)) {
71             throw new mnet_server_exception(1, 'authfail_nosessionexists');
72         }
74         // check session confirm timeout
75         if ($mnet_session->confirm_timeout < time()) {
76             throw new mnet_server_exception(2, 'authfail_sessiontimedout');
77         }
79         // session okay, try getting the user
80         if (!$user = $DB->get_record('user', array('id'=>$mnet_session->userid))) {
81             throw new mnet_server_exception(3, 'authfail_usermismatch');
82         }
84         $userdata = mnet_strip_user((array)$user, mnet_fields_to_send($remoteclient));
86         // extra special ones
87         $userdata['auth']                    = 'mnet';
88         $userdata['wwwroot']                 = $this->mnet->wwwroot;
89         $userdata['session.gc_maxlifetime']  = ini_get('session.gc_maxlifetime');
91         if (array_key_exists('picture', $userdata) && !empty($user->picture)) {
92             $fs = get_file_storage();
93             $usercontext = context_user::instance($user->id, MUST_EXIST);
94             if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
95                 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
96                 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
97             } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
98                 $userdata['_mnet_userpicture_timemodified'] = $usericonfile->get_timemodified();
99                 $userdata['_mnet_userpicture_mimetype'] = $usericonfile->get_mimetype();
100             }
101         }
103         $userdata['myhosts'] = array();
104         if ($courses = enrol_get_users_courses($user->id, false)) {
105             $userdata['myhosts'][] = array('name'=> $SITE->shortname, 'url' => $CFG->wwwroot, 'count' => count($courses));
106         }
108         $sql = "SELECT h.name AS hostname, h.wwwroot, h.id AS hostid,
109                        COUNT(c.id) AS count
110                   FROM {mnetservice_enrol_courses} c
111                   JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
112                   JOIN {mnet_host} h ON h.id = c.hostid
113                  WHERE e.userid = ? AND c.hostid = ?
114               GROUP BY h.name, h.wwwroot, h.id";
116         if ($courses = $DB->get_records_sql($sql, array($user->id, $remoteclient->id))) {
117             foreach($courses as $course) {
118                 $userdata['myhosts'][] = array('name'=> $course->hostname, 'url' => $CFG->wwwroot.'/auth/mnet/jump.php?hostid='.$course->hostid, 'count' => $course->count);
119             }
120         }
122         return $userdata;
123     }
125     /**
126      * Generate a random string for use as an RPC session token.
127      */
128     function generate_token() {
129         return sha1(str_shuffle('' . mt_rand() . time()));
130     }
132     /**
133      * Starts an RPC jump session and returns the jump redirect URL.
134      *
135      * @param int $mnethostid id of the mnet host to jump to
136      * @param string $wantsurl url to redirect to after the jump (usually on remote system)
137      * @param boolean $wantsurlbackhere defaults to false, means that the remote system should bounce us back here
138      *                                  rather than somewhere inside *its* wwwroot
139      */
140     function start_jump_session($mnethostid, $wantsurl, $wantsurlbackhere=false) {
141         global $CFG, $USER, $DB;
142         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
144         if (\core\session\manager::is_loggedinas()) {
145             print_error('notpermittedtojumpas', 'mnet');
146         }
148         // check remote login permissions
149         if (! has_capability('moodle/site:mnetlogintoremote', context_system::instance())
150                 or is_mnet_remote_user($USER)
151                 or isguestuser()
152                 or !isloggedin()) {
153             print_error('notpermittedtojump', 'mnet');
154         }
156         // check for SSO publish permission first
157         if ($this->has_service($mnethostid, 'sso_sp') == false) {
158             print_error('hostnotconfiguredforsso', 'mnet');
159         }
161         // set RPC timeout to 30 seconds if not configured
162         if (empty($this->config->rpc_negotiation_timeout)) {
163             $this->config->rpc_negotiation_timeout = 30;
164             set_config('rpc_negotiation_timeout', '30', 'auth_mnet');
165         }
167         // get the host info
168         $mnet_peer = new mnet_peer();
169         $mnet_peer->set_id($mnethostid);
171         // set up the session
172         $mnet_session = $DB->get_record('mnet_session',
173                                    array('userid'=>$USER->id, 'mnethostid'=>$mnethostid,
174                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])));
175         if ($mnet_session == false) {
176             $mnet_session = new stdClass();
177             $mnet_session->mnethostid = $mnethostid;
178             $mnet_session->userid = $USER->id;
179             $mnet_session->username = $USER->username;
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             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
186         } else {
187             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
188             $mnet_session->token = $this->generate_token();
189             $mnet_session->confirm_timeout = time() + $this->config->rpc_negotiation_timeout;
190             $mnet_session->expires = time() + (integer)ini_get('session.gc_maxlifetime');
191             $mnet_session->session_id = session_id();
192             $DB->update_record('mnet_session', $mnet_session);
193         }
195         // construct the redirection URL
196         //$transport = mnet_get_protocol($mnet_peer->transport);
197         $wantsurl = urlencode($wantsurl);
198         $url = "{$mnet_peer->wwwroot}{$mnet_peer->application->sso_land_url}?token={$mnet_session->token}&idp={$this->mnet->wwwroot}&wantsurl={$wantsurl}";
199         if ($wantsurlbackhere) {
200             $url .= '&remoteurl=1';
201         }
203         return $url;
204     }
206     /**
207      * This function confirms the remote (ID provider) host's mnet session
208      * by communicating the token and UA over the XMLRPC transport layer, and
209      * returns the local user record on success.
210      *
211      *   @param string    $token           The random session token.
212      *   @param mnet_peer $remotepeer   The ID provider mnet_peer object.
213      *   @return array The local user record.
214      */
215     function confirm_mnet_session($token, $remotepeer) {
216         global $CFG, $DB;
217         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
218         require_once $CFG->libdir . '/gdlib.php';
219         require_once($CFG->dirroot.'/user/lib.php');
221         // verify the remote host is configured locally before attempting RPC call
222         if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
223             print_error('notpermittedtoland', 'mnet');
224         }
226         // set up the RPC request
227         $mnetrequest = new mnet_xmlrpc_client();
228         $mnetrequest->set_method('auth/mnet/auth.php/user_authorise');
230         // set $token and $useragent parameters
231         $mnetrequest->add_param($token);
232         $mnetrequest->add_param(sha1($_SERVER['HTTP_USER_AGENT']));
234         // Thunderbirds are go! Do RPC call and store response
235         if ($mnetrequest->send($remotepeer) === true) {
236             $remoteuser = (object) $mnetrequest->response;
237         } else {
238             foreach ($mnetrequest->error as $errormessage) {
239                 list($code, $message) = array_map('trim',explode(':', $errormessage, 2));
240                 if($code == 702) {
241                     $site = get_site();
242                     print_error('mnet_session_prohibited', 'mnet', $remotepeer->wwwroot, format_string($site->fullname));
243                     exit;
244                 }
245                 $message .= "ERROR $code:<br/>$errormessage<br/>";
246             }
247             print_error("rpcerror", '', '', $message);
248         }
249         unset($mnetrequest);
251         if (empty($remoteuser) or empty($remoteuser->username)) {
252             print_error('unknownerror', 'mnet');
253             exit;
254         }
256         if (user_not_fully_set_up($remoteuser)) {
257             print_error('notenoughidpinfo', 'mnet');
258             exit;
259         }
261         $remoteuser = mnet_strip_user($remoteuser, mnet_fields_to_import($remotepeer));
263         $remoteuser->auth = 'mnet';
264         $remoteuser->wwwroot = $remotepeer->wwwroot;
266         // the user may roam from Moodle 1.x where lang has _utf8 suffix
267         // also, make sure that the lang is actually installed, otherwise set site default
268         if (isset($remoteuser->lang)) {
269             $remoteuser->lang = clean_param(str_replace('_utf8', '', $remoteuser->lang), PARAM_LANG);
270         }
271         if (empty($remoteuser->lang)) {
272             if (!empty($CFG->lang)) {
273                 $remoteuser->lang = $CFG->lang;
274             } else {
275                 $remoteuser->lang = 'en';
276             }
277         }
278         $firsttime = false;
280         // get the local record for the remote user
281         $localuser = $DB->get_record('user', array('username'=>$remoteuser->username, 'mnethostid'=>$remotehost->id));
283         // add the remote user to the database if necessary, and if allowed
284         // TODO: refactor into a separate function
285         if (empty($localuser) || ! $localuser->id) {
286             /*
287             if (empty($this->config->auto_add_remote_users)) {
288                 print_error('nolocaluser', 'mnet');
289             } See MDL-21327   for why this is commented out
290             */
291             $remoteuser->mnethostid = $remotehost->id;
292             $remoteuser->firstaccess = time(); // First time user in this server, grab it here
293             $remoteuser->confirmed = 1;
295             $remoteuser->id = $DB->insert_record('user', $remoteuser);
296             $firsttime = true;
297             $localuser = $remoteuser;
298         }
300         // check sso access control list for permission first
301         if (!$this->can_login_remotely($localuser->username, $remotehost->id)) {
302             print_error('sso_mnet_login_refused', 'mnet', '', array('user'=>$localuser->username, 'host'=>$remotehost->name));
303         }
305         $fs = get_file_storage();
307         // update the local user record with remote user data
308         foreach ((array) $remoteuser as $key => $val) {
310             if ($key == '_mnet_userpicture_timemodified' and empty($CFG->disableuserimages) and isset($remoteuser->picture)) {
311                 // update the user picture if there is a newer verion at the identity provider
312                 $usercontext = context_user::instance($localuser->id, MUST_EXIST);
313                 if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
314                     $localtimemodified = $usericonfile->get_timemodified();
315                 } else if ($usericonfile = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
316                     $localtimemodified = $usericonfile->get_timemodified();
317                 } else {
318                     $localtimemodified = 0;
319                 }
321                 if (!empty($val) and $localtimemodified < $val) {
322                     mnet_debug('refetching the user picture from the identity provider host');
323                     $fetchrequest = new mnet_xmlrpc_client();
324                     $fetchrequest->set_method('auth/mnet/auth.php/fetch_user_image');
325                     $fetchrequest->add_param($localuser->username);
326                     if ($fetchrequest->send($remotepeer) === true) {
327                         if (strlen($fetchrequest->response['f1']) > 0) {
328                             $imagefilename = $CFG->tempdir . '/mnet-usericon-' . $localuser->id;
329                             $imagecontents = base64_decode($fetchrequest->response['f1']);
330                             file_put_contents($imagefilename, $imagecontents);
331                             if ($newrev = process_new_icon($usercontext, 'user', 'icon', 0, $imagefilename)) {
332                                 $localuser->picture = $newrev;
333                             }
334                             unlink($imagefilename);
335                         }
336                         // note that since Moodle 2.0 we ignore $fetchrequest->response['f2']
337                         // the mimetype information provided is ignored and the type of the file is detected
338                         // by process_new_icon()
339                     }
340                 }
341             }
343             if($key == 'myhosts') {
344                 $localuser->mnet_foreign_host_array = array();
345                 foreach($val as $rhost) {
346                     $name  = clean_param($rhost['name'], PARAM_ALPHANUM);
347                     $url   = clean_param($rhost['url'], PARAM_URL);
348                     $count = clean_param($rhost['count'], PARAM_INT);
349                     $url_is_local = stristr($url , $CFG->wwwroot);
350                     if (!empty($name) && !empty($count) && empty($url_is_local)) {
351                         $localuser->mnet_foreign_host_array[] = array('name'  => $name,
352                                                                       'url'   => $url,
353                                                                       'count' => $count);
354                     }
355                 }
356             }
358             $localuser->{$key} = $val;
359         }
361         $localuser->mnethostid = $remotepeer->id;
362         if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
363             $localuser->firstaccess = time();
364         }
365         user_update_user($localuser, false);
367         if (!$firsttime) {
368             // repeat customer! let the IDP know about enrolments
369             // we have for this user.
370             // set up the RPC request
371             $mnetrequest = new mnet_xmlrpc_client();
372             $mnetrequest->set_method('auth/mnet/auth.php/update_enrolments');
374             // pass username and an assoc array of "my courses"
375             // with info so that the IDP can maintain mnetservice_enrol_enrolments
376             $mnetrequest->add_param($remoteuser->username);
377             $fields = 'id, category, sortorder, fullname, shortname, idnumber, summary, startdate, visible';
378             $courses = enrol_get_users_courses($localuser->id, false, $fields, 'visible DESC,sortorder ASC');
379             if (is_array($courses) && !empty($courses)) {
380                 // Second request to do the JOINs that we'd have done
381                 // inside enrol_get_users_courses() if we had been allowed
382                 $sql = "SELECT c.id,
383                                cc.name AS cat_name, cc.description AS cat_description
384                           FROM {course} c
385                           JOIN {course_categories} cc ON c.category = cc.id
386                          WHERE c.id IN (" . join(',',array_keys($courses)) . ')';
387                 $extra = $DB->get_records_sql($sql);
389                 $keys = array_keys($courses);
390                 $studentroles = get_archetype_roles('student');
391                 if (!empty($studentroles)) {
392                     $defaultrole = reset($studentroles);
393                     //$defaultrole = get_default_course_role($ccache[$shortname]); //TODO: rewrite this completely, there is no default course role any more!!!
394                     foreach ($keys AS $id) {
395                         if ($courses[$id]->visible == 0) {
396                             unset($courses[$id]);
397                             continue;
398                         }
399                         $courses[$id]->cat_id          = $courses[$id]->category;
400                         $courses[$id]->defaultroleid   = $defaultrole->id;
401                         unset($courses[$id]->category);
402                         unset($courses[$id]->visible);
404                         $courses[$id]->cat_name        = $extra[$id]->cat_name;
405                         $courses[$id]->cat_description = $extra[$id]->cat_description;
406                         $courses[$id]->defaultrolename = $defaultrole->name;
407                         // coerce to array
408                         $courses[$id] = (array)$courses[$id];
409                     }
410                 } else {
411                     throw new moodle_exception('unknownrole', 'error', '', 'student');
412                 }
413             } else {
414                 // if the array is empty, send it anyway
415                 // we may be clearing out stale entries
416                 $courses = array();
417             }
418             $mnetrequest->add_param($courses);
420             // Call 0800-RPC Now! -- we don't care too much if it fails
421             // as it's just informational.
422             if ($mnetrequest->send($remotepeer) === false) {
423                 // error_log(print_r($mnetrequest->error,1));
424             }
425         }
427         return $localuser;
428     }
431     /**
432      * creates (or updates) the mnet session once
433      * {@see confirm_mnet_session} and {@see complete_user_login} have both been called
434      *
435      * @param stdclass  $user the local user (must exist already
436      * @param string    $token the jump/land token
437      * @param mnet_peer $remotepeer the mnet_peer object of this users's idp
438      */
439     public function update_mnet_session($user, $token, $remotepeer) {
440         global $DB;
441         $session_gc_maxlifetime = 1440;
442         if (isset($user->session_gc_maxlifetime)) {
443             $session_gc_maxlifetime = $user->session_gc_maxlifetime;
444         }
445         if (!$mnet_session = $DB->get_record('mnet_session',
446                                    array('userid'=>$user->id, 'mnethostid'=>$remotepeer->id,
447                                    'useragent'=>sha1($_SERVER['HTTP_USER_AGENT'])))) {
448             $mnet_session = new stdClass();
449             $mnet_session->mnethostid = $remotepeer->id;
450             $mnet_session->userid = $user->id;
451             $mnet_session->username = $user->username;
452             $mnet_session->useragent = sha1($_SERVER['HTTP_USER_AGENT']);
453             $mnet_session->token = $token; // Needed to support simultaneous sessions
454                                            // and preserving DB rec uniqueness
455             $mnet_session->confirm_timeout = time();
456             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
457             $mnet_session->session_id = session_id();
458             $mnet_session->id = $DB->insert_record('mnet_session', $mnet_session);
459         } else {
460             $mnet_session->expires = time() + (integer)$session_gc_maxlifetime;
461             $DB->update_record('mnet_session', $mnet_session);
462         }
463     }
467     /**
468      * Invoke this function _on_ the IDP to update it with enrolment info local to
469      * the SP right after calling user_authorise()
470      *
471      * Normally called by the SP after calling user_authorise()
472      *
473      * @param string $username The username
474      * @param array $courses  Assoc array of courses following the structure of mnetservice_enrol_courses
475      * @return bool
476      */
477     function update_enrolments($username, $courses) {
478         global $CFG, $DB;
479         $remoteclient = get_mnet_remote_client();
481         if (empty($username) || !is_array($courses)) {
482             return false;
483         }
484         // make sure it is a user we have an in active session
485         // with that host...
486         $mnetsessions = $DB->get_records('mnet_session', array('username' => $username, 'mnethostid' => $remoteclient->id), '', 'id, userid');
487         $userid = null;
488         foreach ($mnetsessions as $mnetsession) {
489             if (is_null($userid)) {
490                 $userid = $mnetsession->userid;
491                 continue;
492             }
493             if ($userid != $mnetsession->userid) {
494                 throw new mnet_server_exception(3, 'authfail_usermismatch');
495             }
496         }
498         if (empty($courses)) { // no courses? clear out quickly
499             $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$remoteclient->id, 'userid'=>$userid));
500             return true;
501         }
503         // IMPORTANT: Ask for remoteid as the first element in the query, so
504         // that the array that comes back is indexed on the same field as the
505         // array that we have received from the remote client
506         $sql = "SELECT c.remoteid, c.id, c.categoryid AS cat_id, c.categoryname AS cat_name, c.sortorder,
507                        c.fullname, c.shortname, c.idnumber, c.summary, c.summaryformat, c.startdate,
508                        e.id AS enrolmentid
509                   FROM {mnetservice_enrol_courses} c
510              LEFT JOIN {mnetservice_enrol_enrolments} e ON (e.hostid = c.hostid AND e.remotecourseid = c.remoteid)
511                  WHERE e.userid = ? AND c.hostid = ?";
513         $currentcourses = $DB->get_records_sql($sql, array($userid, $remoteclient->id));
515         $local_courseid_array = array();
516         foreach($courses as $ix => $course) {
518             $course['remoteid'] = $course['id'];
519             $course['hostid']   =  (int)$remoteclient->id;
520             $userisregd         = false;
522             // if we do not have the the information about the remote course, it is not available
523             // to us for remote enrolment - skip
524             if (array_key_exists($course['remoteid'], $currentcourses)) {
525                 // Pointer to current course:
526                 $currentcourse =& $currentcourses[$course['remoteid']];
527                 // We have a record - is it up-to-date?
528                 $course['id'] = $currentcourse->id;
530                 $saveflag = false;
532                 foreach($course as $key => $value) {
533                     if ($currentcourse->$key != $value) {
534                         $saveflag = true;
535                         $currentcourse->$key = $value;
536                     }
537                 }
539                 if ($saveflag) {
540                     $DB->update_record('mnetervice_enrol_courses', $currentcourse);
541                 }
543                 if (isset($currentcourse->enrolmentid) && is_numeric($currentcourse->enrolmentid)) {
544                     $userisregd = true;
545                 }
546             } else {
547                 unset ($courses[$ix]);
548                 continue;
549             }
551             // By this point, we should always have a $dataObj->id
552             $local_courseid_array[] = $course['id'];
554             // Do we have a record for this assignment?
555             if ($userisregd) {
556                 // Yes - we know about this one already
557                 // We don't want to do updates because the new data is probably
558                 // 'less complete' than the data we have.
559             } else {
560                 // No - create a record
561                 $assignObj = new stdClass();
562                 $assignObj->userid    = $userid;
563                 $assignObj->hostid    = (int)$remoteclient->id;
564                 $assignObj->remotecourseid = $course['remoteid'];
565                 $assignObj->rolename  = $course['defaultrolename'];
566                 $assignObj->id = $DB->insert_record('mnetservice_enrol_enrolments', $assignObj);
567             }
568         }
570         // Clean up courses that the user is no longer enrolled in.
571         if (!empty($local_courseid_array)) {
572             $local_courseid_string = implode(', ', $local_courseid_array);
573             $whereclause = " userid = ? AND hostid = ? AND remotecourseid NOT IN ($local_courseid_string)";
574             $DB->delete_records_select('mnetservice_enrol_enrolments', $whereclause, array($userid, $remoteclient->id));
575         }
576     }
578     function prevent_local_passwords() {
579         return true;
580     }
582     /**
583      * Returns true if this authentication plugin is 'internal'.
584      *
585      * @return bool
586      */
587     function is_internal() {
588         return false;
589     }
591     /**
592      * Returns true if this authentication plugin can change the user's
593      * password.
594      *
595      * @return bool
596      */
597     function can_change_password() {
598         //TODO: it should be able to redirect, right?
599         return false;
600     }
602     /**
603      * Returns the URL for changing the user's pw, or false if the default can
604      * be used.
605      *
606      * @return moodle_url
607      */
608     function change_password_url() {
609         return null;
610     }
612     /**
613      * Prints a form for configuring this authentication plugin.
614      *
615      * This function is called from admin/auth.php, and outputs a full page with
616      * a form for configuring this plugin.
617      *
618      * @param object $config
619      * @param object $err
620      * @param array $user_fields
621      */
622     function config_form($config, $err, $user_fields) {
623         global $CFG, $DB;
625          $query = "
626             SELECT
627                 h.id,
628                 h.name as hostname,
629                 h.wwwroot,
630                 h2idp.publish as idppublish,
631                 h2idp.subscribe as idpsubscribe,
632                 idp.name as idpname,
633                 h2sp.publish as sppublish,
634                 h2sp.subscribe as spsubscribe,
635                 sp.name as spname
636             FROM
637                 {mnet_host} h
638             LEFT JOIN
639                 {mnet_host2service} h2idp
640             ON
641                (h.id = h2idp.hostid AND
642                (h2idp.publish = 1 OR
643                 h2idp.subscribe = 1))
644             INNER JOIN
645                 {mnet_service} idp
646             ON
647                (h2idp.serviceid = idp.id AND
648                 idp.name = 'sso_idp')
649             LEFT JOIN
650                 {mnet_host2service} h2sp
651             ON
652                (h.id = h2sp.hostid AND
653                (h2sp.publish = 1 OR
654                 h2sp.subscribe = 1))
655             INNER JOIN
656                 {mnet_service} sp
657             ON
658                (h2sp.serviceid = sp.id AND
659                 sp.name = 'sso_sp')
660             WHERE
661                ((h2idp.publish = 1 AND h2sp.subscribe = 1) OR
662                (h2sp.publish = 1 AND h2idp.subscribe = 1)) AND
663                 h.id != ?
664             ORDER BY
665                 h.name ASC";
667         $id_providers       = array();
668         $service_providers  = array();
669         if ($resultset = $DB->get_records_sql($query, array($CFG->mnet_localhost_id))) {
670             foreach($resultset as $hostservice) {
671                 if(!empty($hostservice->idppublish) && !empty($hostservice->spsubscribe)) {
672                     $service_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
673                 }
674                 if(!empty($hostservice->idpsubscribe) && !empty($hostservice->sppublish)) {
675                     $id_providers[]= array('id' => $hostservice->id, 'name' => $hostservice->hostname, 'wwwroot' => $hostservice->wwwroot);
676                 }
677             }
678         }
680         include "config.html";
681     }
683     /**
684      * Processes and stores configuration data for this authentication plugin.
685      */
686     function process_config($config) {
687         // set to defaults if undefined
688         if (!isset ($config->rpc_negotiation_timeout)) {
689             $config->rpc_negotiation_timeout = '30';
690         }
691         /*
692         if (!isset ($config->auto_add_remote_users)) {
693             $config->auto_add_remote_users = '0';
694         } See MDL-21327   for why this is commented out
695         set_config('auto_add_remote_users',   $config->auto_add_remote_users,   'auth_mnet');
696         */
698         // save settings
699         set_config('rpc_negotiation_timeout', $config->rpc_negotiation_timeout, 'auth_mnet');
701         return true;
702     }
704     /**
705      * Poll the IdP server to let it know that a user it has authenticated is still
706      * online
707      *
708      * @return  void
709      */
710     function keepalive_client() {
711         global $CFG, $DB;
712         $cutoff = time() - 300; // TODO - find out what the remote server's session
713                                 // cutoff is, and preempt that
715         $sql = "
716             select
717                 id,
718                 username,
719                 mnethostid
720             from
721                 {user}
722             where
723                 lastaccess > ? AND
724                 mnethostid != ?
725             order by
726                 mnethostid";
728         $immigrants = $DB->get_records_sql($sql, array($cutoff, $CFG->mnet_localhost_id));
730         if ($immigrants == false) {
731             return true;
732         }
734         $usersArray = array();
735         foreach($immigrants as $immigrant) {
736             $usersArray[$immigrant->mnethostid][] = $immigrant->username;
737         }
739         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
740         foreach($usersArray as $mnethostid => $users) {
741             $mnet_peer = new mnet_peer();
742             $mnet_peer->set_id($mnethostid);
744             $mnet_request = new mnet_xmlrpc_client();
745             $mnet_request->set_method('auth/mnet/auth.php/keepalive_server');
747             // set $token and $useragent parameters
748             $mnet_request->add_param($users);
750             if ($mnet_request->send($mnet_peer) === true) {
751                 if (!isset($mnet_request->response['code'])) {
752                     debugging("Server side error has occured on host $mnethostid");
753                     continue;
754                 } elseif ($mnet_request->response['code'] > 0) {
755                     debugging($mnet_request->response['message']);
756                 }
758                 if (!isset($mnet_request->response['last log id'])) {
759                     debugging("Server side error has occured on host $mnethostid\nNo log ID was received.");
760                     continue;
761                 }
762             } else {
763                 debugging("Server side error has occured on host $mnethostid: " .
764                           join("\n", $mnet_request->error));
765                 break;
766             }
767         }
768     }
770     /**
771      * Receives an array of log entries from an SP and adds them to the mnet_log
772      * table
773      *
774      * @deprecated since Moodle 2.8 Please don't use this function for recording mnet logs.
775      * @param   array   $array      An array of usernames
776      * @return  string              "All ok" or an error message
777      */
778     function refresh_log($array) {
779         debugging('refresh_log() is deprecated, The transfer of logs through mnet are no longer recorded.', DEBUG_DEVELOPER);
780         return array('code' => 0, 'message' => 'All ok');
781     }
783     /**
784      * Receives an array of usernames from a remote machine and prods their
785      * sessions to keep them alive
786      *
787      * @param   array   $array      An array of usernames
788      * @return  string              "All ok" or an error message
789      */
790     function keepalive_server($array) {
791         global $CFG, $DB;
792         $remoteclient = get_mnet_remote_client();
794         // We don't want to output anything to the client machine
795         $start = ob_start();
797         // We'll get session records in batches of 30
798         $superArray = array_chunk($array, 30);
800         $returnString = '';
802         foreach($superArray as $subArray) {
803             $subArray = array_values($subArray);
804             $instring = "('".implode("', '",$subArray)."')";
805             $query = "select id, session_id, username from {mnet_session} where username in $instring";
806             $results = $DB->get_records_sql($query);
808             if ($results == false) {
809                 // We seem to have a username that breaks our query:
810                 // TODO: Handle this error appropriately
811                 $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
812             } else {
813                 foreach($results as $emigrant) {
814                     \core\session\manager::touch_session($emigrant->session_id);
815                 }
816             }
817         }
819         $end = ob_end_clean();
821         if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
822         return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
823     }
825     /**
826      * Cron function will be called automatically by cron.php every 5 minutes
827      *
828      * @return void
829      */
830     function cron() {
831         global $DB;
833         // run the keepalive client
834         $this->keepalive_client();
836         $random100 = rand(0,100);
837         if ($random100 < 10) {     // Approximately 10% of the time.
838             // nuke olden sessions
839             $longtime = time() - (1 * 3600 * 24);
840             $DB->delete_records_select('mnet_session', "expires < ?", array($longtime));
841         }
842     }
844     /**
845      * Cleanup any remote mnet_sessions, kill the local mnet_session data
846      *
847      * This is called by require_logout in moodlelib
848      *
849      * @return   void
850      */
851     function prelogout_hook() {
852         global $CFG, $USER;
854         if (!is_enabled_auth('mnet')) {
855             return;
856         }
858         // If the user is local to this Moodle:
859         if ($USER->mnethostid == $this->mnet->id) {
860             $this->kill_children($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
862         // Else the user has hit 'logout' at a Service Provider Moodle:
863         } else {
864             $this->kill_parent($USER->username, sha1($_SERVER['HTTP_USER_AGENT']));
866         }
867     }
869     /**
870      * The SP uses this function to kill the session on the parent IdP
871      *
872      * @param   string  $username       Username for session to kill
873      * @param   string  $useragent      SHA1 hash of user agent to look for
874      * @return  string                  A plaintext report of what has happened
875      */
876     function kill_parent($username, $useragent) {
877         global $CFG, $USER, $DB;
879         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
880         $sql = "
881             select
882                 *
883             from
884                 {mnet_session} s
885             where
886                 s.username   = ? AND
887                 s.useragent  = ? AND
888                 s.mnethostid = ?";
890         $mnetsessions = $DB->get_records_sql($sql, array($username, $useragent, $USER->mnethostid));
892         $ignore = $DB->delete_records('mnet_session',
893                                  array('username'=>$username,
894                                  'useragent'=>$useragent,
895                                  'mnethostid'=>$USER->mnethostid));
897         if (false != $mnetsessions) {
898             $mnet_peer = new mnet_peer();
899             $mnet_peer->set_id($USER->mnethostid);
901             $mnet_request = new mnet_xmlrpc_client();
902             $mnet_request->set_method('auth/mnet/auth.php/kill_children');
904             // set $token and $useragent parameters
905             $mnet_request->add_param($username);
906             $mnet_request->add_param($useragent);
907             if ($mnet_request->send($mnet_peer) === false) {
908                 debugging(join("\n", $mnet_request->error));
909                 return false;
910             }
911         }
913         return true;
914     }
916     /**
917      * The IdP uses this function to kill child sessions on other hosts
918      *
919      * @param   string  $username       Username for session to kill
920      * @param   string  $useragent      SHA1 hash of user agent to look for
921      * @return  string                  A plaintext report of what has happened
922      */
923     function kill_children($username, $useragent) {
924         global $CFG, $USER, $DB;
925         $remoteclient = null;
926         if (defined('MNET_SERVER')) {
927             $remoteclient = get_mnet_remote_client();
928         }
929         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
931         $userid = $DB->get_field('user', 'id', array('mnethostid'=>$CFG->mnet_localhost_id, 'username'=>$username));
933         $returnstring = '';
935         $mnetsessions = $DB->get_records('mnet_session', array('userid' => $userid, 'useragent' => $useragent));
937         if (false == $mnetsessions) {
938             $returnstring .= "Could find no remote sessions\n";
939             $mnetsessions = array();
940         }
942         foreach($mnetsessions as $mnetsession) {
943             // If this script is being executed by a remote peer, that means the user has clicked
944             // logout on that peer, and the session on that peer can be deleted natively.
945             // Skip over it.
946             if (isset($remoteclient->id) && ($mnetsession->mnethostid == $remoteclient->id)) {
947                 continue;
948             }
949             $returnstring .=  "Deleting session\n";
951             $mnet_peer = new mnet_peer();
952             $mnet_peer->set_id($mnetsession->mnethostid);
954             $mnet_request = new mnet_xmlrpc_client();
955             $mnet_request->set_method('auth/mnet/auth.php/kill_child');
957             // set $token and $useragent parameters
958             $mnet_request->add_param($username);
959             $mnet_request->add_param($useragent);
960             if ($mnet_request->send($mnet_peer) === false) {
961                 debugging("Server side error has occured on host $mnetsession->mnethostid: " .
962                           join("\n", $mnet_request->error));
963             }
964         }
966         $ignore = $DB->delete_records('mnet_session',
967                                  array('useragent'=>$useragent, 'userid'=>$userid));
969         if (isset($remoteclient) && isset($remoteclient->id)) {
970             \core\session\manager::kill_user_sessions($userid);
971         }
972         return $returnstring;
973     }
975     /**
976      * When the IdP requests that child sessions are terminated,
977      * this function will be called on each of the child hosts. The machine that
978      * calls the function (over xmlrpc) provides us with the mnethostid we need.
979      *
980      * @param   string  $username       Username for session to kill
981      * @param   string  $useragent      SHA1 hash of user agent to look for
982      * @return  bool                    True on success
983      */
984     function kill_child($username, $useragent) {
985         global $CFG, $DB;
986         $remoteclient = get_mnet_remote_client();
987         $session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
988         $DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
989         if (false != $session) {
990             \core\session\manager::kill_session($session->session_id);
991             return true;
992         }
993         return false;
994     }
996     /**
997      * To delete a host, we must delete all current sessions that users from
998      * that host are currently engaged in.
999      *
1000      * @param   string  $sessionidarray   An array of session hashes
1001      * @return  bool                      True on success
1002      */
1003     function end_local_sessions(&$sessionArray) {
1004         global $CFG;
1005         if (is_array($sessionArray)) {
1006             while($session = array_pop($sessionArray)) {
1007                 \core\session\manager::kill_session($session->session_id);
1008             }
1009             return true;
1010         }
1011         return false;
1012     }
1014     /**
1015      * Returns the user's profile image info
1016      *
1017      * If the user exists and has a profile picture, the returned array will contain keys:
1018      *  f1          - the content of the default 100x100px image
1019      *  f1_mimetype - the mimetype of the f1 file
1020      *  f2          - the content of the 35x35px variant of the image
1021      *  f2_mimetype - the mimetype of the f2 file
1022      *
1023      * The mimetype information was added in Moodle 2.0. In Moodle 1.x, images are always jpegs.
1024      *
1025      * @see process_new_icon()
1026      * @uses mnet_remote_client callable via MNet XML-RPC
1027      * @param int $userid The id of the user
1028      * @return false|array false if user not found, empty array if no picture exists, array with data otherwise
1029      */
1030     function fetch_user_image($username) {
1031         global $CFG, $DB;
1033         if ($user = $DB->get_record('user', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id))) {
1034             $fs = get_file_storage();
1035             $usercontext = context_user::instance($user->id, MUST_EXIST);
1036             $return = array();
1037             if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.png')) {
1038                 $return['f1'] = base64_encode($f1->get_content());
1039                 $return['f1_mimetype'] = $f1->get_mimetype();
1040             } else if ($f1 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
1041                 $return['f1'] = base64_encode($f1->get_content());
1042                 $return['f1_mimetype'] = $f1->get_mimetype();
1043             }
1044             if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.png')) {
1045                 $return['f2'] = base64_encode($f2->get_content());
1046                 $return['f2_mimetype'] = $f2->get_mimetype();
1047             } else if ($f2 = $fs->get_file($usercontext->id, 'user', 'icon', 0, '/', 'f2.jpg')) {
1048                 $return['f2'] = base64_encode($f2->get_content());
1049                 $return['f2_mimetype'] = $f2->get_mimetype();
1050             }
1051             return $return;
1052         }
1053         return false;
1054     }
1056     /**
1057      * Returns the theme information and logo url as strings.
1058      *
1059      * @return string     The theme info
1060      */
1061     function fetch_theme_info() {
1062         global $CFG;
1064         $themename = "$CFG->theme";
1065         $logourl   = "$CFG->wwwroot/theme/$CFG->theme/images/logo.jpg";
1067         $return['themename'] = $themename;
1068         $return['logourl'] = $logourl;
1069         return $return;
1070     }
1072     /**
1073      * Determines if an MNET host is providing the nominated service.
1074      *
1075      * @param int    $mnethostid   The id of the remote host
1076      * @param string $servicename  The name of the service
1077      * @return bool                Whether the service is available on the remote host
1078      */
1079     function has_service($mnethostid, $servicename) {
1080         global $CFG, $DB;
1082         $sql = "
1083             SELECT
1084                 svc.id as serviceid,
1085                 svc.name,
1086                 svc.description,
1087                 svc.offer,
1088                 svc.apiversion,
1089                 h2s.id as h2s_id
1090             FROM
1091                 {mnet_host} h,
1092                 {mnet_service} svc,
1093                 {mnet_host2service} h2s
1094             WHERE
1095                 h.deleted = '0' AND
1096                 h.id = h2s.hostid AND
1097                 h2s.hostid = ? AND
1098                 h2s.serviceid = svc.id AND
1099                 svc.name = ? AND
1100                 h2s.subscribe = '1'";
1102         return $DB->get_records_sql($sql, array($mnethostid, $servicename));
1103     }
1105     /**
1106      * Checks the MNET access control table to see if the username/mnethost
1107      * is permitted to login to this moodle.
1108      *
1109      * @param string $username   The username
1110      * @param int    $mnethostid The id of the remote mnethost
1111      * @return bool              Whether the user can login from the remote host
1112      */
1113     function can_login_remotely($username, $mnethostid) {
1114         global $DB;
1116         $accessctrl = 'allow';
1117         $aclrecord = $DB->get_record('mnet_sso_access_control', array('username'=>$username, 'mnet_host_id'=>$mnethostid));
1118         if (!empty($aclrecord)) {
1119             $accessctrl = $aclrecord->accessctrl;
1120         }
1121         return $accessctrl == 'allow';
1122     }
1124     function logoutpage_hook() {
1125         global $USER, $CFG, $redirect, $DB;
1127         if (!empty($USER->mnethostid) and $USER->mnethostid != $CFG->mnet_localhost_id) {
1128             $host = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid));
1129             $redirect = $host->wwwroot.'/';
1130         }
1131     }
1133     /**
1134      * Trims a log line from mnet peer to limit each part to a length which can be stored in our DB
1135      *
1136      * @param object $logline The log information to be trimmed
1137      * @return object The passed logline object trimmed to not exceed storable limits
1138      */
1139     function trim_logline ($logline) {
1140         $limits = array('ip' => 15, 'coursename' => 40, 'module' => 20, 'action' => 40,
1141                         'url' => 255);
1142         foreach ($limits as $property => $limit) {
1143             if (isset($logline->$property)) {
1144                 $logline->$property = substr($logline->$property, 0, $limit);
1145             }
1146         }
1148         return $logline;
1149     }
1151     /**
1152      * Returns a list of potential IdPs that this authentication plugin supports.
1153      * This is used to provide links on the login page.
1154      *
1155      * @param string $wantsurl the relative url fragment the user wants to get to.  You can use this to compose a returnurl, for example
1156      *
1157      * @return array like:
1158      *              array(
1159      *                  array(
1160      *                      'url' => 'http://someurl',
1161      *                      'icon' => new pix_icon(...),
1162      *                      'name' => get_string('somename', 'auth_yourplugin'),
1163      *                 ),
1164      *             )
1165      */
1166     function loginpage_idp_list($wantsurl) {
1167         global $DB, $CFG;
1169         // strip off wwwroot, since the remote site will prefix it's return url with this
1170         $wantsurl = preg_replace('/(' . preg_quote($CFG->wwwroot, '/') . '|' . preg_quote($CFG->httpswwwroot, '/') . ')/', '', $wantsurl);
1172         $sql = "SELECT DISTINCT h.id, h.wwwroot, h.name, a.sso_jump_url, a.name as application
1173                   FROM {mnet_host} h
1174                   JOIN {mnet_host2service} m ON h.id = m.hostid
1175                   JOIN {mnet_service} s ON s.id = m.serviceid
1176                   JOIN {mnet_application} a ON h.applicationid = a.id
1177                  WHERE s.name = ? AND h.deleted = ? AND m.publish = ?";
1178         $params = array('sso_sp', 0, 1);
1180         if (!empty($CFG->mnet_all_hosts_id)) {
1181             $sql .= " AND h.id <> ?";
1182             $params[] = $CFG->mnet_all_hosts_id;
1183         }
1185         if (!$hosts = $DB->get_records_sql($sql, $params)) {
1186             return array();
1187         }
1189         $idps = array();
1190         foreach ($hosts as $host) {
1191             $idps[] = array(
1192                 'url'  => new moodle_url($host->wwwroot . $host->sso_jump_url, array('hostwwwroot' => $CFG->wwwroot, 'wantsurl' => $wantsurl, 'remoteurl' => 1)),
1193                 'icon' => new pix_icon('i/' . $host->application . '_host', $host->name),
1194                 'name' => $host->name,
1195             );
1196         }
1197         return $idps;
1198     }