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