on-demand release 4.0dev+
[moodle.git] / mnet / service / enrol / locallib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Provides various useful functionality to plugins that offer or use this MNet service
20  *
21  * Remote enrolment service is used by enrol_mnet plugin which publishes the server side
22  * methods. The client side is accessible from the admin tree.
23  *
24  * @package    mnetservice
25  * @subpackage enrol
26  * @copyright  2010 David Mudrak <david@moodle.com>
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
30 defined('MOODLE_INTERNAL') || die();
32 require_once($CFG->dirroot . '/user/selector/lib.php');
34 /**
35  * Singleton providing various functionality usable by plugin(s) implementing this MNet service
36  */
37 class mnetservice_enrol {
39     /** @var mnetservice_enrol holds the singleton instance. */
40     protected static $singleton;
42     /** @var caches the result of {@link self::get_remote_subscribers()} */
43     protected $cachesubscribers = null;
45     /** @var caches the result of {@link self::get_remote_publishers()} */
46     protected $cachepublishers = null;
48     /**
49      * This is singleton, use {@link mnetservice_enrol::get_instance()}
50      */
51     protected function __construct() {
52     }
54     /**
55      * @return mnetservice_enrol singleton instance
56      */
57     public static function get_instance() {
58         if (is_null(self::$singleton)) {
59             self::$singleton = new self();
60         }
61         return self::$singleton;
62     }
64     /**
65      * Is this service enabled?
66      *
67      * Currently, this checks if whole MNet is available. In the future, additional
68      * checks can be done. Probably the field 'offer' should be checked but it does
69      * not seem to be used so far.
70      *
71      * @todo move this to some parent class once we have such
72      * @return bool
73      */
74     public function is_available() {
75         global $CFG;
77         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
78             return false;
79         }
80         return true;
81     }
83     /**
84      * Returns a list of remote servers that can enrol their users into our courses
85      *
86      * We must publish MNet service 'mnet_enrol' for the peers to allow them to enrol
87      * their users into our courses.
88      *
89      * @todo once the MNet core is refactored this may be part of a parent class
90      * @todo the name of the service should be changed to the name of this plugin
91      * @return array
92      */
93     public function get_remote_subscribers() {
94         global $DB;
96         if (is_null($this->cachesubscribers)) {
97             $sql = "SELECT DISTINCT h.id, h.name AS hostname, h.wwwroot AS hosturl,
98                            a.display_name AS appname
99                       FROM {mnet_host} h
100                       JOIN {mnet_host2service} hs ON h.id = hs.hostid
101                       JOIN {mnet_service} s ON hs.serviceid = s.id
102                       JOIN {mnet_application} a ON h.applicationid = a.id
103                      WHERE s.name = 'mnet_enrol'
104                            AND h.deleted = 0
105                            AND hs.publish = 1";
106             $this->cachesubscribers = $DB->get_records_sql($sql);
107         }
109         return $this->cachesubscribers;
110     }
112     /**
113      * Returns a list of remote servers that offer their courses for our users
114      *
115      * We must subscribe MNet service 'mnet_enrol' for the peers to allow our users to enrol
116      * into their courses.
117      *
118      * @todo once the MNet core is refactored this may be part of a parent class
119      * @todo the name of the service should be changed to the name of this plugin
120      * @return array
121      */
122     public function get_remote_publishers() {
123         global $DB;
125         if (is_null($this->cachepublishers)) {
126             $sql = "SELECT DISTINCT h.id, h.name AS hostname, h.wwwroot AS hosturl,
127                            a.display_name AS appname
128                       FROM {mnet_host} h
129                       JOIN {mnet_host2service} hs ON h.id = hs.hostid
130                       JOIN {mnet_service} s ON hs.serviceid = s.id
131                       JOIN {mnet_application} a ON h.applicationid = a.id
132                      WHERE s.name = 'mnet_enrol'
133                            AND h.deleted = 0
134                            AND hs.subscribe = 1";
135             $this->cachepublishers = $DB->get_records_sql($sql);
136         }
138         return $this->cachepublishers;
139     }
141     /**
142      * Fetches the information about the courses available on remote host for our students
143      *
144      * The information about remote courses available for us is cached in {mnetservice_enrol_courses}.
145      * This method either returns the cached information (typically when displaying the list to
146      * students) or fetch fresh data via new XML-RPC request (which updates the local cache, too).
147      * The lifetime of the cache is 1 day, so even if $usecache is set to true, the cache will be
148      * re-populated if we did not fetch from any server (not only the currently requested one)
149      * for some time.
150      *
151      * @param id $mnethostid MNet remote host id
152      * @param bool $usecache use cached data or invoke new XML-RPC?
153      * @uses mnet_xmlrpc_client Invokes XML-RPC request if the cache is not used
154      * @return array|string returned list or serialized array of mnet error messages
155      */
156     public function get_remote_courses($mnethostid, $usecache=true) {
157         global $CFG, $DB; // $CFG needed!
159         $lastfetchcourses = get_config('mnetservice_enrol', 'lastfetchcourses');
160         if (empty($lastfetchcourses) or (time()-$lastfetchcourses > DAYSECS)) {
161             $usecache = false;
162         }
164         if ($usecache) {
165             return $DB->get_records('mnetservice_enrol_courses', array('hostid' => $mnethostid), 'sortorder, shortname');
166         }
168         // do not use cache - fetch fresh list from remote MNet host
169         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
170         $peer = new mnet_peer();
171         if (!$peer->set_id($mnethostid)) {
172             return serialize(array('unknown mnet peer'));
173         }
175         $request = new mnet_xmlrpc_client();
176         $request->set_method('enrol/mnet/enrol.php/available_courses');
178         if ($request->send($peer)) {
179             $list = array();
180             $response = $request->response;
182             // get the currently cached courses key'd on remote id - only need remoteid and id fields
183             $cachedcourses = $DB->get_records('mnetservice_enrol_courses', array('hostid' => $mnethostid), 'remoteid', 'remoteid, id');
185             foreach ($response as &$remote) {
186                 $course                 = new stdclass(); // record in our local cache
187                 $course->hostid         = $mnethostid;
188                 $course->remoteid       = (int)$remote['remoteid'];
189                 $course->categoryid     = (int)$remote['cat_id'];
190                 $course->categoryname   = substr($remote['cat_name'], 0, 255);
191                 $course->sortorder      = (int)$remote['sortorder'];
192                 $course->fullname       = substr($remote['fullname'], 0, 254);
193                 $course->shortname      = substr($remote['shortname'], 0, 100);
194                 $course->idnumber       = substr($remote['idnumber'], 0, 100);
195                 $course->summary        = $remote['summary'];
196                 $course->summaryformat  = empty($remote['summaryformat']) ? FORMAT_MOODLE : (int)$remote['summaryformat'];
197                 $course->startdate      = (int)$remote['startdate'];
198                 $course->roleid         = (int)$remote['defaultroleid'];
199                 $course->rolename       = substr($remote['defaultrolename'], 0, 255);
200                 // We do not cache the following fields returned from peer in 2.0 any more
201                 // not cached: cat_description
202                 // not cached: cat_descriptionformat
203                 // not cached: cost
204                 // not cached: currency
206                 if (empty($cachedcourses[$course->remoteid])) {
207                     $course->id = $DB->insert_record('mnetservice_enrol_courses', $course);
208                 } else {
209                     $course->id = $cachedcourses[$course->remoteid]->id;
210                     $DB->update_record('mnetservice_enrol_courses', $course);
211                 }
213                 $list[$course->remoteid] = $course;
214             }
216             // prune stale data from cache
217             if (!empty($cachedcourses)) {
218                 foreach ($cachedcourses as $cachedcourse) {
219                     if (!empty($list[$cachedcourse->remoteid])) {
220                         unset($cachedcourses[$cachedcourse->remoteid]);
221                     }
222                 }
223                 $staleremoteids = array_keys($cachedcourses);
224                 if (!empty($staleremoteids)) {
225                     list($sql, $params) = $DB->get_in_or_equal($staleremoteids, SQL_PARAMS_NAMED);
226                     $select = "hostid=:hostid AND remoteid $sql";
227                     $params['hostid'] = $mnethostid;
228                     $DB->delete_records_select('mnetservice_enrol_courses', $select, $params);
229                 }
230             }
232             // and return the fresh data
233             set_config('lastfetchcourses', time(), 'mnetservice_enrol');
234             return $list;
236         } else {
237             return serialize($request->error);
238         }
239     }
241     /**
242      * Updates local cache about enrolments of our users in remote courses
243      *
244      * The remote course must allow enrolments via our Remote enrolment service client.
245      * Because of legacy design of data structure returned by XML-RPC code, only one
246      * user enrolment per course is returned by 1.9 MNet servers. This may be an issue
247      * if the user is enrolled multiple times by various enrolment plugins. MNet 2.0
248      * servers do not use user name as array keys - they do not need to due to side
249      * effect of MDL-19219.
250      *
251      * @param id $mnethostid MNet remote host id
252      * @param int $remotecourseid ID of the course at the remote host
253      * @param bool $usecache use cached data or invoke new XML-RPC?
254      * @uses mnet_xmlrpc_client Invokes XML-RPC request
255      * @return bool|string true if success or serialized array of mnet error messages
256      */
257     public function req_course_enrolments($mnethostid, $remotecourseid) {
258         global $CFG, $DB; // $CFG needed!
259         require_once $CFG->dirroot.'/mnet/xmlrpc/client.php';
261         if (!$DB->record_exists('mnetservice_enrol_courses', array('hostid'=>$mnethostid, 'remoteid'=>$remotecourseid))) {
262             return serialize(array('course not available for remote enrolments'));
263         }
265         $peer = new mnet_peer();
266         if (!$peer->set_id($mnethostid)) {
267             return serialize(array('unknown mnet peer'));
268         }
270         $request = new mnet_xmlrpc_client();
271         $request->set_method('enrol/mnet/enrol.php/course_enrolments');
272         $request->add_param($remotecourseid, 'int');
274         if ($request->send($peer)) {
275             $list = array();
276             $response = $request->response;
278             // prepare a table mapping usernames of our users to their ids
279             $usernames = array();
280             foreach ($response as $unused => $remote) {
281                 if (!isset($remote['username'])) {
282                     // see MDL-19219
283                     return serialize(array('remote host running old version of mnet server - does not return username attribute'));
284                 }
285                 if ($remote['username'] == 'guest') { // we can not use $CFG->siteguest here
286                     // do not try nasty things you bastard!
287                     continue;
288                 }
289                 $usernames[$remote['username']] = $remote['username'];
290             }
292             if (!empty($usernames)) {
293                 list($usql, $params) = $DB->get_in_or_equal($usernames, SQL_PARAMS_NAMED);
294                 list($sort, $sortparams) = users_order_by_sql();
295                 $params['mnetlocalhostid'] = $CFG->mnet_localhost_id;
296                 $sql = "SELECT username,id
297                           FROM {user}
298                          WHERE mnethostid = :mnetlocalhostid
299                                AND username $usql
300                                AND deleted = 0
301                                AND confirmed = 1
302                       ORDER BY $sort";
303                 $usersbyusername = $DB->get_records_sql($sql, array_merge($params, $sortparams));
304             } else {
305                 $usersbyusername = array();
306             }
308             // populate the returned list and update local cache of enrolment records
309             foreach ($response as $remote) {
310                 if (empty($usersbyusername[$remote['username']])) {
311                     // we do not know this user or she is deleted or not confirmed or is 'guest'
312                     continue;
313                 }
314                 $enrolment                  = new stdclass();
315                 $enrolment->hostid          = $mnethostid;
316                 $enrolment->userid          = $usersbyusername[$remote['username']]->id;
317                 $enrolment->remotecourseid  = $remotecourseid;
318                 $enrolment->rolename        = $remote['name']; // $remote['shortname'] not used
319                 $enrolment->enroltime       = $remote['timemodified'];
320                 $enrolment->enroltype       = $remote['enrol'];
322                 $current = $DB->get_record('mnetservice_enrol_enrolments', array('hostid'=>$enrolment->hostid, 'userid'=>$enrolment->userid,
323                                        'remotecourseid'=>$enrolment->remotecourseid, 'enroltype'=>$enrolment->enroltype), 'id, enroltime');
324                 if (empty($current)) {
325                     $enrolment->id = $DB->insert_record('mnetservice_enrol_enrolments', $enrolment);
326                 } else {
327                     $enrolment->id = $current->id;
328                     if ($current->enroltime != $enrolment->enroltime) {
329                         $DB->update_record('mnetservice_enrol_enrolments', $enrolment);
330                     }
331                 }
333                 $list[$enrolment->id] = $enrolment;
334             }
336             // prune stale enrolment records
337             if (empty($list)) {
338                 $DB->delete_records('mnetservice_enrol_enrolments', array('hostid'=>$mnethostid, 'remotecourseid'=>$remotecourseid));
339             } else {
340                 list($isql, $params) = $DB->get_in_or_equal(array_keys($list), SQL_PARAMS_NAMED, 'param', false);
341                 $params['hostid'] = $mnethostid;
342                 $params['remotecourseid'] = $remotecourseid;
343                 $select = "hostid = :hostid AND remotecourseid = :remotecourseid AND id $isql";
344                 $DB->delete_records_select('mnetservice_enrol_enrolments', $select, $params);
345             }
347             // store the timestamp of the recent fetch, can be used for cache invalidate purposes
348             set_config('lastfetchenrolments', time(), 'mnetservice_enrol');
349             // local cache successfully updated
350             return true;
352         } else {
353             return serialize($request->error);
354         }
355     }
357     /**
358      * Send request to enrol our user to the remote course
359      *
360      * Updates our remote enrolments cache if the enrolment was successful.
361      *
362      * @uses mnet_xmlrpc_client Invokes XML-RPC request
363      * @param object $user our user
364      * @param object $remotecourse record from mnetservice_enrol_courses table
365      * @return true|string true if success, error message from the remote host otherwise
366      */
367     public function req_enrol_user(stdclass $user, stdclass $remotecourse) {
368         global $CFG, $DB;
369         require_once($CFG->dirroot.'/mnet/xmlrpc/client.php');
371         $peer = new mnet_peer();
372         $peer->set_id($remotecourse->hostid);
374         $request = new mnet_xmlrpc_client();
375         $request->set_method('enrol/mnet/enrol.php/enrol_user');
376         $request->add_param(mnet_strip_user((array)$user, mnet_fields_to_send($peer)));
377         $request->add_param($remotecourse->remoteid);
379         if ($request->send($peer) === true) {
380             if ($request->response === true) {
381                 // cache the enrolment information in our table
382                 $enrolment                  = new stdclass();
383                 $enrolment->hostid          = $peer->id;
384                 $enrolment->userid          = $user->id;
385                 $enrolment->remotecourseid  = $remotecourse->remoteid;
386                 $enrolment->enroltype       = 'mnet';
387                 // $enrolment->rolename not known now, must be re-fetched
388                 // $enrolment->enroltime not known now, must be re-fetched
389                 $DB->insert_record('mnetservice_enrol_enrolments', $enrolment);
390                 return true;
392             } else {
393                 return serialize(array('invalid response: '.print_r($request->response, true)));
394             }
396         } else {
397             return serialize($request->error);
398         }
399     }
401     /**
402      * Send request to unenrol our user from the remote course
403      *
404      * Updates our remote enrolments cache if the unenrolment was successful.
405      *
406      * @uses mnet_xmlrpc_client Invokes XML-RPC request
407      * @param object $user our user
408      * @param object $remotecourse record from mnetservice_enrol_courses table
409      * @return true|string true if success, error message from the remote host otherwise
410      */
411     public function req_unenrol_user(stdclass $user, stdclass $remotecourse) {
412         global $CFG, $DB;
413         require_once($CFG->dirroot.'/mnet/xmlrpc/client.php');
415         $peer = new mnet_peer();
416         $peer->set_id($remotecourse->hostid);
418         $request = new mnet_xmlrpc_client();
419         $request->set_method('enrol/mnet/enrol.php/unenrol_user');
420         $request->add_param($user->username);
421         $request->add_param($remotecourse->remoteid);
423         if ($request->send($peer) === true) {
424             if ($request->response === true) {
425                 // clear the cached information
426                 $DB->delete_records('mnetservice_enrol_enrolments',
427                     array('hostid'=>$peer->id, 'userid'=>$user->id, 'remotecourseid'=>$remotecourse->remoteid, 'enroltype'=>'mnet'));
428                 return true;
430             } else {
431                 return serialize(array('invalid response: '.print_r($request->response, true)));
432             }
434         } else {
435             return serialize($request->error);
436         }
437     }
439     /**
440      * Prepares error messages returned by our XML-RPC requests to be send as debug info to {@link print_error()}
441      *
442      * MNet client-side methods in this class return request error as serialized array.
443      *
444      * @param string $error serialized array
445      * @return string
446      */
447     public function format_error_message($errormsg) {
448         $errors = unserialize($errormsg);
449         $output = 'mnet_xmlrpc_client request returned errors:'."\n";
450         foreach ($errors as $error) {
451             $output .= "$error\n";
452         }
453         return $output;
454     }
457 /**
458  * Selector of our users enrolled into remote course via enrol_mnet plugin
459  */
460 class mnetservice_enrol_existing_users_selector extends user_selector_base {
461     /** @var id of the MNet peer */
462     protected $hostid;
463     /** @var id of the course at the remote server */
464     protected $remotecourseid;
466     public function __construct($name, $options) {
467         $this->hostid = $options['hostid'];
468         $this->remotecourseid = $options['remotecourseid'];
469         parent::__construct($name, $options);
470     }
472     /**
473      * Find our users currently enrolled into the remote course
474      *
475      * @param string $search
476      * @return array
477      */
478     public function find_users($search) {
479         global $DB;
481         list($wherecondition, $params)  = $this->search_sql($search, 'u');
482         $params['hostid']               = $this->hostid;
483         $params['remotecourseid']       = $this->remotecourseid;
485         $fields      = "SELECT ".$this->required_fields_sql("u");
486         $countfields = "SELECT COUNT(1)";
488         $sql = "          FROM {user} u
489                           JOIN {mnetservice_enrol_enrolments} e ON e.userid = u.id
490                          WHERE e.hostid = :hostid AND e.remotecourseid = :remotecourseid
491                                AND e.enroltype = 'mnet'
492                                AND $wherecondition";
494         list($sort, $sortparams) = users_order_by_sql('u');
495         $order = "    ORDER BY $sort";
497         if (!$this->is_validating()) {
498             $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
499             if ($potentialmemberscount > 100) {
500                 return $this->too_many_results($search, $potentialmemberscount);
501             }
502         }
504         $availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams));
506         if (empty($availableusers)) {
507             return array();
508         }
510         if ($search) {
511             $groupname = get_string('enrolledusersmatching', 'enrol', $search);
512         } else {
513             $groupname = get_string('enrolledusers', 'enrol');
514         }
516         return array($groupname => $availableusers);
517     }
519     protected function get_options() {
520         $options = parent::get_options();
521         $options['hostid'] = $this->hostid;
522         $options['remotecourseid'] = $this->remotecourseid;
523         $options['file'] = 'mnet/service/enrol/locallib.php';
524         return $options;
525     }
528 /**
529  * Selector of our users who could be enrolled into a remote course via their enrol_mnet
530  */
531 class mnetservice_enrol_potential_users_selector extends user_selector_base {
532     /** @var id of the MNet peer */
533     protected $hostid;
534     /** @var id of the course at the remote server */
535     protected $remotecourseid;
537     public function __construct($name, $options) {
538         $this->hostid = $options['hostid'];
539         $this->remotecourseid = $options['remotecourseid'];
540         parent::__construct($name, $options);
541     }
543     /**
544      * Find our users who could be enrolled into the remote course
545      *
546      * Our users must have 'moodle/site:mnetlogintoremote' capability assigned.
547      * Remote users, guests, deleted and not confirmed users are not returned.
548      *
549      * @param string $search
550      * @return array
551      */
552     public function find_users($search) {
553         global $CFG, $DB;
555         $systemcontext = context_system::instance();
556         $userids = get_users_by_capability($systemcontext, 'moodle/site:mnetlogintoremote', 'u.id');
558         if (empty($userids)) {
559             return array();
560         }
562         list($usql, $uparams) = $DB->get_in_or_equal(array_keys($userids), SQL_PARAMS_NAMED, 'uid');
564         list($wherecondition, $params) = $this->search_sql($search, 'u');
566         $params = array_merge($params, $uparams);
567         $params['hostid'] = $this->hostid;
568         $params['remotecourseid'] = $this->remotecourseid;
569         $params['mnetlocalhostid'] = $CFG->mnet_localhost_id;
571         $fields      = "SELECT ".$this->required_fields_sql("u");
572         $countfields = "SELECT COUNT(1)";
574         $sql = "          FROM {user} u
575                          WHERE $wherecondition
576                                AND u.mnethostid = :mnetlocalhostid
577                                AND u.id $usql
578                                AND u.id NOT IN (SELECT e.userid
579                                                   FROM {mnetservice_enrol_enrolments} e
580                                                  WHERE (e.hostid = :hostid AND e.remotecourseid = :remotecourseid))";
582         list($sort, $sortparams) = users_order_by_sql('u');
583         $order = "    ORDER BY $sort";
585         if (!$this->is_validating()) {
586             $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
587             if ($potentialmemberscount > 100) {
588                 return $this->too_many_results($search, $potentialmemberscount);
589             }
590         }
592         $availableusers = $DB->get_records_sql($fields . $sql . $order, array_merge($params, $sortparams));
594         if (empty($availableusers)) {
595             return array();
596         }
598         if ($search) {
599             $groupname = get_string('enrolcandidatesmatching', 'enrol', $search);
600         } else {
601             $groupname = get_string('enrolcandidates', 'enrol');
602         }
604         return array($groupname => $availableusers);
605     }
607     protected function get_options() {
608         $options = parent::get_options();
609         $options['hostid'] = $this->hostid;
610         $options['remotecourseid'] = $this->remotecourseid;
611         $options['file'] = 'mnet/service/enrol/locallib.php';
612         return $options;
613     }