dc7aa9224c9e21b8abaa5c0d0bc5b7d166c07848
[moodle.git] / lib / enrollib.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  * This library includes the basic parts of enrol api.
20  * It is available on each page.
21  *
22  * @package    core
23  * @subpackage enrol
24  * @copyright  2010 Petr Skoda {@link http://skodak.org}
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
28 defined('MOODLE_INTERNAL') || die();
30 /** Course enrol instance enabled. (used in enrol->status) */
31 define('ENROL_INSTANCE_ENABLED', 0);
33 /** Course enrol instance disabled, user may enter course if other enrol instance enabled. (used in enrol->status)*/
34 define('ENROL_INSTANCE_DISABLED', 1);
36 /** User is active participant (used in user_enrolments->status)*/
37 define('ENROL_USER_ACTIVE', 0);
39 /** User participation in course is suspended (used in user_enrolments->status) */
40 define('ENROL_USER_SUSPENDED', 1);
42 /** @deprecated - enrol caching was reworked, use ENROL_MAX_TIMESTAMP instead */
43 define('ENROL_REQUIRE_LOGIN_CACHE_PERIOD', 1800);
45 /** The timestamp indicating forever */
46 define('ENROL_MAX_TIMESTAMP', 2147483647);
48 /** When user disappears from external source, the enrolment is completely removed */
49 define('ENROL_EXT_REMOVED_UNENROL', 0);
51 /** When user disappears from external source, the enrolment is kept as is - one way sync */
52 define('ENROL_EXT_REMOVED_KEEP', 1);
54 /** @deprecated since 2.4 not used any more, migrate plugin to new restore methods */
55 define('ENROL_RESTORE_TYPE', 'enrolrestore');
57 /**
58  * When user disappears from external source, user enrolment is suspended, roles are kept as is.
59  * In some cases user needs a role with some capability to be visible in UI - suc has in gradebook,
60  * assignments, etc.
61  */
62 define('ENROL_EXT_REMOVED_SUSPEND', 2);
64 /**
65  * When user disappears from external source, the enrolment is suspended and roles assigned
66  * by enrol instance are removed. Please note that user may "disappear" from gradebook and other areas.
67  * */
68 define('ENROL_EXT_REMOVED_SUSPENDNOROLES', 3);
70 /**
71  * Returns instances of enrol plugins
72  * @param bool $enabled return enabled only
73  * @return array of enrol plugins name=>instance
74  */
75 function enrol_get_plugins($enabled) {
76     global $CFG;
78     $result = array();
80     if ($enabled) {
81         // sorted by enabled plugin order
82         $enabled = explode(',', $CFG->enrol_plugins_enabled);
83         $plugins = array();
84         foreach ($enabled as $plugin) {
85             $plugins[$plugin] = "$CFG->dirroot/enrol/$plugin";
86         }
87     } else {
88         // sorted alphabetically
89         $plugins = core_component::get_plugin_list('enrol');
90         ksort($plugins);
91     }
93     foreach ($plugins as $plugin=>$location) {
94         if (!file_exists("$location/lib.php")) {
95             continue;
96         }
97         include_once("$location/lib.php");
98         $class = "enrol_{$plugin}_plugin";
99         if (!class_exists($class)) {
100             continue;
101         }
103         $result[$plugin] = new $class();
104     }
106     return $result;
109 /**
110  * Returns instance of enrol plugin
111  * @param  string $name name of enrol plugin ('manual', 'guest', ...)
112  * @return enrol_plugin
113  */
114 function enrol_get_plugin($name) {
115     global $CFG;
117     $name = clean_param($name, PARAM_PLUGIN);
119     if (empty($name)) {
120         // ignore malformed or missing plugin names completely
121         return null;
122     }
124     $location = "$CFG->dirroot/enrol/$name";
126     if (!file_exists("$location/lib.php")) {
127         return null;
128     }
129     include_once("$location/lib.php");
130     $class = "enrol_{$name}_plugin";
131     if (!class_exists($class)) {
132         return null;
133     }
135     return new $class();
138 /**
139  * Returns enrolment instances in given course.
140  * @param int $courseid
141  * @param bool $enabled
142  * @return array of enrol instances
143  */
144 function enrol_get_instances($courseid, $enabled) {
145     global $DB, $CFG;
147     if (!$enabled) {
148         return $DB->get_records('enrol', array('courseid'=>$courseid), 'sortorder,id');
149     }
151     $result = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id');
153     $enabled = explode(',', $CFG->enrol_plugins_enabled);
154     foreach ($result as $key=>$instance) {
155         if (!in_array($instance->enrol, $enabled)) {
156             unset($result[$key]);
157             continue;
158         }
159         if (!file_exists("$CFG->dirroot/enrol/$instance->enrol/lib.php")) {
160             // broken plugin
161             unset($result[$key]);
162             continue;
163         }
164     }
166     return $result;
169 /**
170  * Checks if a given plugin is in the list of enabled enrolment plugins.
171  *
172  * @param string $enrol Enrolment plugin name
173  * @return boolean Whether the plugin is enabled
174  */
175 function enrol_is_enabled($enrol) {
176     global $CFG;
178     if (empty($CFG->enrol_plugins_enabled)) {
179         return false;
180     }
181     return in_array($enrol, explode(',', $CFG->enrol_plugins_enabled));
184 /**
185  * Check all the login enrolment information for the given user object
186  * by querying the enrolment plugins
187  *
188  * This function may be very slow, use only once after log-in or login-as.
189  *
190  * @param stdClass $user
191  * @return void
192  */
193 function enrol_check_plugins($user) {
194     global $CFG;
196     if (empty($user->id) or isguestuser($user)) {
197         // shortcut - there is no enrolment work for guests and not-logged-in users
198         return;
199     }
201     // originally there was a broken admin test, but accidentally it was non-functional in 2.2,
202     // which proved it was actually not necessary.
204     static $inprogress = array();  // To prevent this function being called more than once in an invocation
206     if (!empty($inprogress[$user->id])) {
207         return;
208     }
210     $inprogress[$user->id] = true;  // Set the flag
212     $enabled = enrol_get_plugins(true);
214     foreach($enabled as $enrol) {
215         $enrol->sync_user_enrolments($user);
216     }
218     unset($inprogress[$user->id]);  // Unset the flag
221 /**
222  * Do these two students share any course?
223  *
224  * The courses has to be visible and enrolments has to be active,
225  * timestart and timeend restrictions are ignored.
226  *
227  * This function calls {@see enrol_get_shared_courses()} setting checkexistsonly
228  * to true.
229  *
230  * @param stdClass|int $user1
231  * @param stdClass|int $user2
232  * @return bool
233  */
234 function enrol_sharing_course($user1, $user2) {
235     return enrol_get_shared_courses($user1, $user2, false, true);
238 /**
239  * Returns any courses shared by the two users
240  *
241  * The courses has to be visible and enrolments has to be active,
242  * timestart and timeend restrictions are ignored.
243  *
244  * @global moodle_database $DB
245  * @param stdClass|int $user1
246  * @param stdClass|int $user2
247  * @param bool $preloadcontexts If set to true contexts for the returned courses
248  *              will be preloaded.
249  * @param bool $checkexistsonly If set to true then this function will return true
250  *              if the users share any courses and false if not.
251  * @return array|bool An array of courses that both users are enrolled in OR if
252  *              $checkexistsonly set returns true if the users share any courses
253  *              and false if not.
254  */
255 function enrol_get_shared_courses($user1, $user2, $preloadcontexts = false, $checkexistsonly = false) {
256     global $DB, $CFG;
258     $user1 = isset($user1->id) ? $user1->id : $user1;
259     $user2 = isset($user2->id) ? $user2->id : $user2;
261     if (empty($user1) or empty($user2)) {
262         return false;
263     }
265     if (!$plugins = explode(',', $CFG->enrol_plugins_enabled)) {
266         return false;
267     }
269     list($plugins, $params) = $DB->get_in_or_equal($plugins, SQL_PARAMS_NAMED, 'ee');
270     $params['enabled'] = ENROL_INSTANCE_ENABLED;
271     $params['active1'] = ENROL_USER_ACTIVE;
272     $params['active2'] = ENROL_USER_ACTIVE;
273     $params['user1']   = $user1;
274     $params['user2']   = $user2;
276     $ctxselect = '';
277     $ctxjoin = '';
278     if ($preloadcontexts) {
279         $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
280         $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
281         $params['contextlevel'] = CONTEXT_COURSE;
282     }
284     $sql = "SELECT c.* $ctxselect
285               FROM {course} c
286               JOIN (
287                 SELECT DISTINCT c.id
288                   FROM {enrol} e
289                   JOIN {user_enrolments} ue1 ON (ue1.enrolid = e.id AND ue1.status = :active1 AND ue1.userid = :user1)
290                   JOIN {user_enrolments} ue2 ON (ue2.enrolid = e.id AND ue2.status = :active2 AND ue2.userid = :user2)
291                   JOIN {course} c ON (c.id = e.courseid AND c.visible = 1)
292                  WHERE e.status = :enabled AND e.enrol $plugins
293               ) ec ON ec.id = c.id
294               $ctxjoin";
296     if ($checkexistsonly) {
297         return $DB->record_exists_sql($sql, $params);
298     } else {
299         $courses = $DB->get_records_sql($sql, $params);
300         if ($preloadcontexts) {
301             array_map('context_helper::preload_from_record', $courses);
302         }
303         return $courses;
304     }
307 /**
308  * This function adds necessary enrol plugins UI into the course edit form.
309  *
310  * @param MoodleQuickForm $mform
311  * @param object $data course edit form data
312  * @param object $context context of existing course or parent category if course does not exist
313  * @return void
314  */
315 function enrol_course_edit_form(MoodleQuickForm $mform, $data, $context) {
316     $plugins = enrol_get_plugins(true);
317     if (!empty($data->id)) {
318         $instances = enrol_get_instances($data->id, false);
319         foreach ($instances as $instance) {
320             if (!isset($plugins[$instance->enrol])) {
321                 continue;
322             }
323             $plugin = $plugins[$instance->enrol];
324             $plugin->course_edit_form($instance, $mform, $data, $context);
325         }
326     } else {
327         foreach ($plugins as $plugin) {
328             $plugin->course_edit_form(NULL, $mform, $data, $context);
329         }
330     }
333 /**
334  * Validate course edit form data
335  *
336  * @param array $data raw form data
337  * @param object $context context of existing course or parent category if course does not exist
338  * @return array errors array
339  */
340 function enrol_course_edit_validation(array $data, $context) {
341     $errors = array();
342     $plugins = enrol_get_plugins(true);
344     if (!empty($data['id'])) {
345         $instances = enrol_get_instances($data['id'], false);
346         foreach ($instances as $instance) {
347             if (!isset($plugins[$instance->enrol])) {
348                 continue;
349             }
350             $plugin = $plugins[$instance->enrol];
351             $errors = array_merge($errors, $plugin->course_edit_validation($instance, $data, $context));
352         }
353     } else {
354         foreach ($plugins as $plugin) {
355             $errors = array_merge($errors, $plugin->course_edit_validation(NULL, $data, $context));
356         }
357     }
359     return $errors;
362 /**
363  * Update enrol instances after course edit form submission
364  * @param bool $inserted true means new course added, false course already existed
365  * @param object $course
366  * @param object $data form data
367  * @return void
368  */
369 function enrol_course_updated($inserted, $course, $data) {
370     global $DB, $CFG;
372     $plugins = enrol_get_plugins(true);
374     foreach ($plugins as $plugin) {
375         $plugin->course_updated($inserted, $course, $data);
376     }
379 /**
380  * Add navigation nodes
381  * @param navigation_node $coursenode
382  * @param object $course
383  * @return void
384  */
385 function enrol_add_course_navigation(navigation_node $coursenode, $course) {
386     global $CFG;
388     $coursecontext = context_course::instance($course->id);
390     $instances = enrol_get_instances($course->id, true);
391     $plugins   = enrol_get_plugins(true);
393     // we do not want to break all course pages if there is some borked enrol plugin, right?
394     foreach ($instances as $k=>$instance) {
395         if (!isset($plugins[$instance->enrol])) {
396             unset($instances[$k]);
397         }
398     }
400     $usersnode = $coursenode->add(get_string('users'), null, navigation_node::TYPE_CONTAINER, null, 'users');
402     if ($course->id != SITEID) {
403         // list all participants - allows assigning roles, groups, etc.
404         if (has_capability('moodle/course:enrolreview', $coursecontext)) {
405             $url = new moodle_url('/enrol/users.php', array('id'=>$course->id));
406             $usersnode->add(get_string('enrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'review', new pix_icon('i/enrolusers', ''));
407         }
409         // manage enrol plugin instances
410         if (has_capability('moodle/course:enrolconfig', $coursecontext) or has_capability('moodle/course:enrolreview', $coursecontext)) {
411             $url = new moodle_url('/enrol/instances.php', array('id'=>$course->id));
412         } else {
413             $url = NULL;
414         }
415         $instancesnode = $usersnode->add(get_string('enrolmentinstances', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'manageinstances');
417         // each instance decides how to configure itself or how many other nav items are exposed
418         foreach ($instances as $instance) {
419             if (!isset($plugins[$instance->enrol])) {
420                 continue;
421             }
422             $plugins[$instance->enrol]->add_course_navigation($instancesnode, $instance);
423         }
425         if (!$url) {
426             $instancesnode->trim_if_empty();
427         }
428     }
430     // Manage groups in this course or even frontpage
431     if (($course->groupmode || !$course->groupmodeforce) && has_capability('moodle/course:managegroups', $coursecontext)) {
432         $url = new moodle_url('/group/index.php', array('id'=>$course->id));
433         $usersnode->add(get_string('groups'), $url, navigation_node::TYPE_SETTING, null, 'groups', new pix_icon('i/group', ''));
434     }
436      if (has_any_capability(array( 'moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:review'), $coursecontext)) {
437         // Override roles
438         if (has_capability('moodle/role:review', $coursecontext)) {
439             $url = new moodle_url('/admin/roles/permissions.php', array('contextid'=>$coursecontext->id));
440         } else {
441             $url = NULL;
442         }
443         $permissionsnode = $usersnode->add(get_string('permissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'override');
445         // Add assign or override roles if allowed
446         if ($course->id == SITEID or (!empty($CFG->adminsassignrolesincourse) and is_siteadmin())) {
447             if (has_capability('moodle/role:assign', $coursecontext)) {
448                 $url = new moodle_url('/admin/roles/assign.php', array('contextid'=>$coursecontext->id));
449                 $permissionsnode->add(get_string('assignedroles', 'role'), $url, navigation_node::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
450             }
451         }
452         // Check role permissions
453         if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $coursecontext)) {
454             $url = new moodle_url('/admin/roles/check.php', array('contextid'=>$coursecontext->id));
455             $permissionsnode->add(get_string('checkpermissions', 'role'), $url, navigation_node::TYPE_SETTING, null, 'permissions', new pix_icon('i/checkpermissions', ''));
456         }
457      }
459      // Deal somehow with users that are not enrolled but still got a role somehow
460     if ($course->id != SITEID) {
461         //TODO, create some new UI for role assignments at course level
462         if (has_capability('moodle/role:assign', $coursecontext)) {
463             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
464             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
465         }
466     }
468     // just in case nothing was actually added
469     $usersnode->trim_if_empty();
471     if ($course->id != SITEID) {
472         if (isguestuser() or !isloggedin()) {
473             // guest account can not be enrolled - no links for them
474         } else if (is_enrolled($coursecontext)) {
475             // unenrol link if possible
476             foreach ($instances as $instance) {
477                 if (!isset($plugins[$instance->enrol])) {
478                     continue;
479                 }
480                 $plugin = $plugins[$instance->enrol];
481                 if ($unenrollink = $plugin->get_unenrolself_link($instance)) {
482                     $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
483                     $coursenode->add(get_string('unenrolme', 'core_enrol', $shortname), $unenrollink, navigation_node::TYPE_SETTING, null, 'unenrolself', new pix_icon('i/user', ''));
484                     break;
485                     //TODO. deal with multiple unenrol links - not likely case, but still...
486                 }
487             }
488         } else {
489             // enrol link if possible
490             if (is_viewing($coursecontext)) {
491                 // better not show any enrol link, this is intended for managers and inspectors
492             } else {
493                 foreach ($instances as $instance) {
494                     if (!isset($plugins[$instance->enrol])) {
495                         continue;
496                     }
497                     $plugin = $plugins[$instance->enrol];
498                     if ($plugin->show_enrolme_link($instance)) {
499                         $url = new moodle_url('/enrol/index.php', array('id'=>$course->id));
500                         $shortname = format_string($course->shortname, true, array('context' => $coursecontext));
501                         $coursenode->add(get_string('enrolme', 'core_enrol', $shortname), $url, navigation_node::TYPE_SETTING, null, 'enrolself', new pix_icon('i/user', ''));
502                         break;
503                     }
504                 }
505             }
506         }
507     }
510 /**
511  * Returns list of courses current $USER is enrolled in and can access
512  *
513  * - $fields is an array of field names to ADD
514  *   so name the fields you really need, which will
515  *   be added and uniq'd
516  *
517  * @param string|array $fields
518  * @param string $sort
519  * @param int $limit max number of courses
520  * @return array
521  */
522 function enrol_get_my_courses($fields = NULL, $sort = 'visible DESC,sortorder ASC', $limit = 0) {
523     global $DB, $USER;
525     // Guest account does not have any courses
526     if (isguestuser() or !isloggedin()) {
527         return(array());
528     }
530     $basefields = array('id', 'category', 'sortorder',
531                         'shortname', 'fullname', 'idnumber',
532                         'startdate', 'visible',
533                         'groupmode', 'groupmodeforce', 'cacherev');
535     if (empty($fields)) {
536         $fields = $basefields;
537     } else if (is_string($fields)) {
538         // turn the fields from a string to an array
539         $fields = explode(',', $fields);
540         $fields = array_map('trim', $fields);
541         $fields = array_unique(array_merge($basefields, $fields));
542     } else if (is_array($fields)) {
543         $fields = array_unique(array_merge($basefields, $fields));
544     } else {
545         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
546     }
547     if (in_array('*', $fields)) {
548         $fields = array('*');
549     }
551     $orderby = "";
552     $sort    = trim($sort);
553     if (!empty($sort)) {
554         $rawsorts = explode(',', $sort);
555         $sorts = array();
556         foreach ($rawsorts as $rawsort) {
557             $rawsort = trim($rawsort);
558             if (strpos($rawsort, 'c.') === 0) {
559                 $rawsort = substr($rawsort, 2);
560             }
561             $sorts[] = trim($rawsort);
562         }
563         $sort = 'c.'.implode(',c.', $sorts);
564         $orderby = "ORDER BY $sort";
565     }
567     $wheres = array("c.id <> :siteid");
568     $params = array('siteid'=>SITEID);
570     if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
571         // list _only_ this course - anything else is asking for trouble...
572         $wheres[] = "courseid = :loginas";
573         $params['loginas'] = $USER->loginascontext->instanceid;
574     }
576     $coursefields = 'c.' .join(',c.', $fields);
577     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
578     $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
579     $params['contextlevel'] = CONTEXT_COURSE;
580     $wheres = implode(" AND ", $wheres);
582     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
583     $sql = "SELECT $coursefields $ccselect
584               FROM {course} c
585               JOIN (SELECT DISTINCT e.courseid
586                       FROM {enrol} e
587                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
588                      WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)
589                    ) en ON (en.courseid = c.id)
590            $ccjoin
591              WHERE $wheres
592           $orderby";
593     $params['userid']  = $USER->id;
594     $params['active']  = ENROL_USER_ACTIVE;
595     $params['enabled'] = ENROL_INSTANCE_ENABLED;
596     $params['now1']    = round(time(), -2); // improves db caching
597     $params['now2']    = $params['now1'];
599     $courses = $DB->get_records_sql($sql, $params, 0, $limit);
601     // preload contexts and check visibility
602     foreach ($courses as $id=>$course) {
603         context_helper::preload_from_record($course);
604         if (!$course->visible) {
605             if (!$context = context_course::instance($id, IGNORE_MISSING)) {
606                 unset($courses[$id]);
607                 continue;
608             }
609             if (!has_capability('moodle/course:viewhiddencourses', $context)) {
610                 unset($courses[$id]);
611                 continue;
612             }
613         }
614         $courses[$id] = $course;
615     }
617     //wow! Is that really all? :-D
619     return $courses;
622 /**
623  * Returns course enrolment information icons.
624  *
625  * @param object $course
626  * @param array $instances enrol instances of this course, improves performance
627  * @return array of pix_icon
628  */
629 function enrol_get_course_info_icons($course, array $instances = NULL) {
630     $icons = array();
631     if (is_null($instances)) {
632         $instances = enrol_get_instances($course->id, true);
633     }
634     $plugins = enrol_get_plugins(true);
635     foreach ($plugins as $name => $plugin) {
636         $pis = array();
637         foreach ($instances as $instance) {
638             if ($instance->status != ENROL_INSTANCE_ENABLED or $instance->courseid != $course->id) {
639                 debugging('Invalid instances parameter submitted in enrol_get_info_icons()');
640                 continue;
641             }
642             if ($instance->enrol == $name) {
643                 $pis[$instance->id] = $instance;
644             }
645         }
646         if ($pis) {
647             $icons = array_merge($icons, $plugin->get_info_icons($pis));
648         }
649     }
650     return $icons;
653 /**
654  * Returns course enrolment detailed information.
655  *
656  * @param object $course
657  * @return array of html fragments - can be used to construct lists
658  */
659 function enrol_get_course_description_texts($course) {
660     $lines = array();
661     $instances = enrol_get_instances($course->id, true);
662     $plugins = enrol_get_plugins(true);
663     foreach ($instances as $instance) {
664         if (!isset($plugins[$instance->enrol])) {
665             //weird
666             continue;
667         }
668         $plugin = $plugins[$instance->enrol];
669         $text = $plugin->get_description_text($instance);
670         if ($text !== NULL) {
671             $lines[] = $text;
672         }
673     }
674     return $lines;
677 /**
678  * Returns list of courses user is enrolled into.
679  * (Note: use enrol_get_all_users_courses if you want to use the list wihtout any cap checks )
680  *
681  * - $fields is an array of fieldnames to ADD
682  *   so name the fields you really need, which will
683  *   be added and uniq'd
684  *
685  * @param int $userid
686  * @param bool $onlyactive return only active enrolments in courses user may see
687  * @param string|array $fields
688  * @param string $sort
689  * @return array
690  */
691 function enrol_get_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
692     global $DB;
694     $courses = enrol_get_all_users_courses($userid, $onlyactive, $fields, $sort);
696     // preload contexts and check visibility
697     if ($onlyactive) {
698         foreach ($courses as $id=>$course) {
699             context_helper::preload_from_record($course);
700             if (!$course->visible) {
701                 if (!$context = context_course::instance($id)) {
702                     unset($courses[$id]);
703                     continue;
704                 }
705                 if (!has_capability('moodle/course:viewhiddencourses', $context, $userid)) {
706                     unset($courses[$id]);
707                     continue;
708                 }
709             }
710         }
711     }
713     return $courses;
717 /**
718  * Can user access at least one enrolled course?
719  *
720  * Cheat if necessary, but find out as fast as possible!
721  *
722  * @param int|stdClass $user null means use current user
723  * @return bool
724  */
725 function enrol_user_sees_own_courses($user = null) {
726     global $USER;
728     if ($user === null) {
729         $user = $USER;
730     }
731     $userid = is_object($user) ? $user->id : $user;
733     // Guest account does not have any courses
734     if (isguestuser($userid) or empty($userid)) {
735         return false;
736     }
738     // Let's cheat here if this is the current user,
739     // if user accessed any course recently, then most probably
740     // we do not need to query the database at all.
741     if ($USER->id == $userid) {
742         if (!empty($USER->enrol['enrolled'])) {
743             foreach ($USER->enrol['enrolled'] as $until) {
744                 if ($until > time()) {
745                     return true;
746                 }
747             }
748         }
749     }
751     // Now the slow way.
752     $courses = enrol_get_all_users_courses($userid, true);
753     foreach($courses as $course) {
754         if ($course->visible) {
755             return true;
756         }
757         context_helper::preload_from_record($course);
758         $context = context_course::instance($course->id);
759         if (has_capability('moodle/course:viewhiddencourses', $context, $user)) {
760             return true;
761         }
762     }
764     return false;
767 /**
768  * Returns list of courses user is enrolled into without any capability checks
769  * - $fields is an array of fieldnames to ADD
770  *   so name the fields you really need, which will
771  *   be added and uniq'd
772  *
773  * @param int $userid
774  * @param bool $onlyactive return only active enrolments in courses user may see
775  * @param string|array $fields
776  * @param string $sort
777  * @return array
778  */
779 function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NULL, $sort = 'visible DESC,sortorder ASC') {
780     global $DB;
782     // Guest account does not have any courses
783     if (isguestuser($userid) or empty($userid)) {
784         return(array());
785     }
787     $basefields = array('id', 'category', 'sortorder',
788             'shortname', 'fullname', 'idnumber',
789             'startdate', 'visible',
790             'groupmode', 'groupmodeforce');
792     if (empty($fields)) {
793         $fields = $basefields;
794     } else if (is_string($fields)) {
795         // turn the fields from a string to an array
796         $fields = explode(',', $fields);
797         $fields = array_map('trim', $fields);
798         $fields = array_unique(array_merge($basefields, $fields));
799     } else if (is_array($fields)) {
800         $fields = array_unique(array_merge($basefields, $fields));
801     } else {
802         throw new coding_exception('Invalid $fileds parameter in enrol_get_my_courses()');
803     }
804     if (in_array('*', $fields)) {
805         $fields = array('*');
806     }
808     $orderby = "";
809     $sort    = trim($sort);
810     if (!empty($sort)) {
811         $rawsorts = explode(',', $sort);
812         $sorts = array();
813         foreach ($rawsorts as $rawsort) {
814             $rawsort = trim($rawsort);
815             if (strpos($rawsort, 'c.') === 0) {
816                 $rawsort = substr($rawsort, 2);
817             }
818             $sorts[] = trim($rawsort);
819         }
820         $sort = 'c.'.implode(',c.', $sorts);
821         $orderby = "ORDER BY $sort";
822     }
824     $params = array('siteid'=>SITEID);
826     if ($onlyactive) {
827         $subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
828         $params['now1']    = round(time(), -2); // improves db caching
829         $params['now2']    = $params['now1'];
830         $params['active']  = ENROL_USER_ACTIVE;
831         $params['enabled'] = ENROL_INSTANCE_ENABLED;
832     } else {
833         $subwhere = "";
834     }
836     $coursefields = 'c.' .join(',c.', $fields);
837     $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
838     $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
839     $params['contextlevel'] = CONTEXT_COURSE;
841     //note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why we have the subselect there
842     $sql = "SELECT $coursefields $ccselect
843               FROM {course} c
844               JOIN (SELECT DISTINCT e.courseid
845                       FROM {enrol} e
846                       JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
847                  $subwhere
848                    ) en ON (en.courseid = c.id)
849            $ccjoin
850              WHERE c.id <> :siteid
851           $orderby";
852     $params['userid']  = $userid;
854     $courses = $DB->get_records_sql($sql, $params);
856     return $courses;
861 /**
862  * Called when user is about to be deleted.
863  * @param object $user
864  * @return void
865  */
866 function enrol_user_delete($user) {
867     global $DB;
869     $plugins = enrol_get_plugins(true);
870     foreach ($plugins as $plugin) {
871         $plugin->user_delete($user);
872     }
874     // force cleanup of all broken enrolments
875     $DB->delete_records('user_enrolments', array('userid'=>$user->id));
878 /**
879  * Called when course is about to be deleted.
880  * @param stdClass $course
881  * @return void
882  */
883 function enrol_course_delete($course) {
884     global $DB;
886     $instances = enrol_get_instances($course->id, false);
887     $plugins = enrol_get_plugins(true);
888     foreach ($instances as $instance) {
889         if (isset($plugins[$instance->enrol])) {
890             $plugins[$instance->enrol]->delete_instance($instance);
891         }
892         // low level delete in case plugin did not do it
893         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
894         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$instance->enrol));
895         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
896         $DB->delete_records('enrol', array('id'=>$instance->id));
897     }
900 /**
901  * Try to enrol user via default internal auth plugin.
902  *
903  * For now this is always using the manual enrol plugin...
904  *
905  * @param $courseid
906  * @param $userid
907  * @param $roleid
908  * @param $timestart
909  * @param $timeend
910  * @return bool success
911  */
912 function enrol_try_internal_enrol($courseid, $userid, $roleid = null, $timestart = 0, $timeend = 0) {
913     global $DB;
915     //note: this is hardcoded to manual plugin for now
917     if (!enrol_is_enabled('manual')) {
918         return false;
919     }
921     if (!$enrol = enrol_get_plugin('manual')) {
922         return false;
923     }
924     if (!$instances = $DB->get_records('enrol', array('enrol'=>'manual', 'courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder,id ASC')) {
925         return false;
926     }
927     $instance = reset($instances);
929     $enrol->enrol_user($instance, $userid, $roleid, $timestart, $timeend);
931     return true;
934 /**
935  * Is there a chance users might self enrol
936  * @param int $courseid
937  * @return bool
938  */
939 function enrol_selfenrol_available($courseid) {
940     $result = false;
942     $plugins = enrol_get_plugins(true);
943     $enrolinstances = enrol_get_instances($courseid, true);
944     foreach($enrolinstances as $instance) {
945         if (!isset($plugins[$instance->enrol])) {
946             continue;
947         }
948         if ($instance->enrol === 'guest') {
949             // blacklist known temporary guest plugins
950             continue;
951         }
952         if ($plugins[$instance->enrol]->show_enrolme_link($instance)) {
953             $result = true;
954             break;
955         }
956     }
958     return $result;
961 /**
962  * This function returns the end of current active user enrolment.
963  *
964  * It deals correctly with multiple overlapping user enrolments.
965  *
966  * @param int $courseid
967  * @param int $userid
968  * @return int|bool timestamp when active enrolment ends, false means no active enrolment now, 0 means never
969  */
970 function enrol_get_enrolment_end($courseid, $userid) {
971     global $DB;
973     $sql = "SELECT ue.*
974               FROM {user_enrolments} ue
975               JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
976               JOIN {user} u ON u.id = ue.userid
977              WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
978     $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$courseid);
980     if (!$enrolments = $DB->get_records_sql($sql, $params)) {
981         return false;
982     }
984     $changes = array();
986     foreach ($enrolments as $ue) {
987         $start = (int)$ue->timestart;
988         $end = (int)$ue->timeend;
989         if ($end != 0 and $end < $start) {
990             debugging('Invalid enrolment start or end in user_enrolment id:'.$ue->id);
991             continue;
992         }
993         if (isset($changes[$start])) {
994             $changes[$start] = $changes[$start] + 1;
995         } else {
996             $changes[$start] = 1;
997         }
998         if ($end === 0) {
999             // no end
1000         } else if (isset($changes[$end])) {
1001             $changes[$end] = $changes[$end] - 1;
1002         } else {
1003             $changes[$end] = -1;
1004         }
1005     }
1007     // let's sort then enrolment starts&ends and go through them chronologically,
1008     // looking for current status and the next future end of enrolment
1009     ksort($changes);
1011     $now = time();
1012     $current = 0;
1013     $present = null;
1015     foreach ($changes as $time => $change) {
1016         if ($time > $now) {
1017             if ($present === null) {
1018                 // we have just went past current time
1019                 $present = $current;
1020                 if ($present < 1) {
1021                     // no enrolment active
1022                     return false;
1023                 }
1024             }
1025             if ($present !== null) {
1026                 // we are already in the future - look for possible end
1027                 if ($current + $change < 1) {
1028                     return $time;
1029                 }
1030             }
1031         }
1032         $current += $change;
1033     }
1035     if ($current > 0) {
1036         return 0;
1037     } else {
1038         return false;
1039     }
1043 /**
1044  * All enrol plugins should be based on this class,
1045  * this is also the main source of documentation.
1046  */
1047 abstract class enrol_plugin {
1048     protected $config = null;
1050     /**
1051      * Returns name of this enrol plugin
1052      * @return string
1053      */
1054     public function get_name() {
1055         // second word in class is always enrol name, sorry, no fancy plugin names with _
1056         $words = explode('_', get_class($this));
1057         return $words[1];
1058     }
1060     /**
1061      * Returns localised name of enrol instance
1062      *
1063      * @param object $instance (null is accepted too)
1064      * @return string
1065      */
1066     public function get_instance_name($instance) {
1067         if (empty($instance->name)) {
1068             $enrol = $this->get_name();
1069             return get_string('pluginname', 'enrol_'.$enrol);
1070         } else {
1071             $context = context_course::instance($instance->courseid);
1072             return format_string($instance->name, true, array('context'=>$context));
1073         }
1074     }
1076     /**
1077      * Returns optional enrolment information icons.
1078      *
1079      * This is used in course list for quick overview of enrolment options.
1080      *
1081      * We are not using single instance parameter because sometimes
1082      * we might want to prevent icon repetition when multiple instances
1083      * of one type exist. One instance may also produce several icons.
1084      *
1085      * @param array $instances all enrol instances of this type in one course
1086      * @return array of pix_icon
1087      */
1088     public function get_info_icons(array $instances) {
1089         return array();
1090     }
1092     /**
1093      * Returns optional enrolment instance description text.
1094      *
1095      * This is used in detailed course information.
1096      *
1097      *
1098      * @param object $instance
1099      * @return string short html text
1100      */
1101     public function get_description_text($instance) {
1102         return null;
1103     }
1105     /**
1106      * Makes sure config is loaded and cached.
1107      * @return void
1108      */
1109     protected function load_config() {
1110         if (!isset($this->config)) {
1111             $name = $this->get_name();
1112             $this->config = get_config("enrol_$name");
1113         }
1114     }
1116     /**
1117      * Returns plugin config value
1118      * @param  string $name
1119      * @param  string $default value if config does not exist yet
1120      * @return string value or default
1121      */
1122     public function get_config($name, $default = NULL) {
1123         $this->load_config();
1124         return isset($this->config->$name) ? $this->config->$name : $default;
1125     }
1127     /**
1128      * Sets plugin config value
1129      * @param  string $name name of config
1130      * @param  string $value string config value, null means delete
1131      * @return string value
1132      */
1133     public function set_config($name, $value) {
1134         $pluginname = $this->get_name();
1135         $this->load_config();
1136         if ($value === NULL) {
1137             unset($this->config->$name);
1138         } else {
1139             $this->config->$name = $value;
1140         }
1141         set_config($name, $value, "enrol_$pluginname");
1142     }
1144     /**
1145      * Does this plugin assign protected roles are can they be manually removed?
1146      * @return bool - false means anybody may tweak roles, it does not use itemid and component when assigning roles
1147      */
1148     public function roles_protected() {
1149         return true;
1150     }
1152     /**
1153      * Does this plugin allow manual enrolments?
1154      *
1155      * @param stdClass $instance course enrol instance
1156      * All plugins allowing this must implement 'enrol/xxx:enrol' capability
1157      *
1158      * @return bool - true means user with 'enrol/xxx:enrol' may enrol others freely, false means nobody may add more enrolments manually
1159      */
1160     public function allow_enrol(stdClass $instance) {
1161         return false;
1162     }
1164     /**
1165      * Does this plugin allow manual unenrolment of all users?
1166      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1167      *
1168      * @param stdClass $instance course enrol instance
1169      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol others freely, false means nobody may touch user_enrolments
1170      */
1171     public function allow_unenrol(stdClass $instance) {
1172         return false;
1173     }
1175     /**
1176      * Does this plugin allow manual unenrolment of a specific user?
1177      * All plugins allowing this must implement 'enrol/xxx:unenrol' capability
1178      *
1179      * This is useful especially for synchronisation plugins that
1180      * do suspend instead of full unenrolment.
1181      *
1182      * @param stdClass $instance course enrol instance
1183      * @param stdClass $ue record from user_enrolments table, specifies user
1184      *
1185      * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
1186      */
1187     public function allow_unenrol_user(stdClass $instance, stdClass $ue) {
1188         return $this->allow_unenrol($instance);
1189     }
1191     /**
1192      * Does this plugin allow manual changes in user_enrolments table?
1193      *
1194      * All plugins allowing this must implement 'enrol/xxx:manage' capability
1195      *
1196      * @param stdClass $instance course enrol instance
1197      * @return bool - true means it is possible to change enrol period and status in user_enrolments table
1198      */
1199     public function allow_manage(stdClass $instance) {
1200         return false;
1201     }
1203     /**
1204      * Does this plugin support some way to user to self enrol?
1205      *
1206      * @param stdClass $instance course enrol instance
1207      *
1208      * @return bool - true means show "Enrol me in this course" link in course UI
1209      */
1210     public function show_enrolme_link(stdClass $instance) {
1211         return false;
1212     }
1214     /**
1215      * Attempt to automatically enrol current user in course without any interaction,
1216      * calling code has to make sure the plugin and instance are active.
1217      *
1218      * This should return either a timestamp in the future or false.
1219      *
1220      * @param stdClass $instance course enrol instance
1221      * @return bool|int false means not enrolled, integer means timeend
1222      */
1223     public function try_autoenrol(stdClass $instance) {
1224         global $USER;
1226         return false;
1227     }
1229     /**
1230      * Attempt to automatically gain temporary guest access to course,
1231      * calling code has to make sure the plugin and instance are active.
1232      *
1233      * This should return either a timestamp in the future or false.
1234      *
1235      * @param stdClass $instance course enrol instance
1236      * @return bool|int false means no guest access, integer means timeend
1237      */
1238     public function try_guestaccess(stdClass $instance) {
1239         global $USER;
1241         return false;
1242     }
1244     /**
1245      * Enrol user into course via enrol instance.
1246      *
1247      * @param stdClass $instance
1248      * @param int $userid
1249      * @param int $roleid optional role id
1250      * @param int $timestart 0 means unknown
1251      * @param int $timeend 0 means forever
1252      * @param int $status default to ENROL_USER_ACTIVE for new enrolments, no change by default in updates
1253      * @param bool $recovergrades restore grade history
1254      * @return void
1255      */
1256     public function enrol_user(stdClass $instance, $userid, $roleid = null, $timestart = 0, $timeend = 0, $status = null, $recovergrades = null) {
1257         global $DB, $USER, $CFG; // CFG necessary!!!
1259         if ($instance->courseid == SITEID) {
1260             throw new coding_exception('invalid attempt to enrol into frontpage course!');
1261         }
1263         $name = $this->get_name();
1264         $courseid = $instance->courseid;
1266         if ($instance->enrol !== $name) {
1267             throw new coding_exception('invalid enrol instance!');
1268         }
1269         $context = context_course::instance($instance->courseid, MUST_EXIST);
1270         if (!isset($recovergrades)) {
1271             $recovergrades = $CFG->recovergradesdefault;
1272         }
1274         $inserted = false;
1275         $updated  = false;
1276         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1277             //only update if timestart or timeend or status are different.
1278             if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
1279                 $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
1280             }
1281         } else {
1282             $ue = new stdClass();
1283             $ue->enrolid      = $instance->id;
1284             $ue->status       = is_null($status) ? ENROL_USER_ACTIVE : $status;
1285             $ue->userid       = $userid;
1286             $ue->timestart    = $timestart;
1287             $ue->timeend      = $timeend;
1288             $ue->modifierid   = $USER->id;
1289             $ue->timecreated  = time();
1290             $ue->timemodified = $ue->timecreated;
1291             $ue->id = $DB->insert_record('user_enrolments', $ue);
1293             $inserted = true;
1294         }
1296         if ($inserted) {
1297             // Trigger event.
1298             $event = \core\event\user_enrolment_created::create(
1299                     array(
1300                         'objectid' => $ue->id,
1301                         'courseid' => $courseid,
1302                         'context' => $context,
1303                         'relateduserid' => $ue->userid,
1304                         'other' => array('enrol' => $name)
1305                         )
1306                     );
1307             $event->trigger();
1308         }
1310         if ($roleid) {
1311             // this must be done after the enrolment event so that the role_assigned event is triggered afterwards
1312             if ($this->roles_protected()) {
1313                 role_assign($roleid, $userid, $context->id, 'enrol_'.$name, $instance->id);
1314             } else {
1315                 role_assign($roleid, $userid, $context->id);
1316             }
1317         }
1319         // Recover old grades if present.
1320         if ($recovergrades) {
1321             require_once("$CFG->libdir/gradelib.php");
1322             grade_recover_history_grades($userid, $courseid);
1323         }
1325         // reset current user enrolment caching
1326         if ($userid == $USER->id) {
1327             if (isset($USER->enrol['enrolled'][$courseid])) {
1328                 unset($USER->enrol['enrolled'][$courseid]);
1329             }
1330             if (isset($USER->enrol['tempguest'][$courseid])) {
1331                 unset($USER->enrol['tempguest'][$courseid]);
1332                 remove_temp_course_roles($context);
1333             }
1334         }
1335     }
1337     /**
1338      * Store user_enrolments changes and trigger event.
1339      *
1340      * @param stdClass $instance
1341      * @param int $userid
1342      * @param int $status
1343      * @param int $timestart
1344      * @param int $timeend
1345      * @return void
1346      */
1347     public function update_user_enrol(stdClass $instance, $userid, $status = NULL, $timestart = NULL, $timeend = NULL) {
1348         global $DB, $USER;
1350         $name = $this->get_name();
1352         if ($instance->enrol !== $name) {
1353             throw new coding_exception('invalid enrol instance!');
1354         }
1356         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1357             // weird, user not enrolled
1358             return;
1359         }
1361         $modified = false;
1362         if (isset($status) and $ue->status != $status) {
1363             $ue->status = $status;
1364             $modified = true;
1365         }
1366         if (isset($timestart) and $ue->timestart != $timestart) {
1367             $ue->timestart = $timestart;
1368             $modified = true;
1369         }
1370         if (isset($timeend) and $ue->timeend != $timeend) {
1371             $ue->timeend = $timeend;
1372             $modified = true;
1373         }
1375         if (!$modified) {
1376             // no change
1377             return;
1378         }
1380         $ue->modifierid = $USER->id;
1381         $DB->update_record('user_enrolments', $ue);
1382         context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
1384         // Trigger event.
1385         $event = \core\event\user_enrolment_updated::create(
1386                 array(
1387                     'objectid' => $ue->id,
1388                     'courseid' => $instance->courseid,
1389                     'context' => context_course::instance($instance->courseid),
1390                     'relateduserid' => $ue->userid,
1391                     'other' => array('enrol' => $name)
1392                     )
1393                 );
1394         $event->trigger();
1395     }
1397     /**
1398      * Unenrol user from course,
1399      * the last unenrolment removes all remaining roles.
1400      *
1401      * @param stdClass $instance
1402      * @param int $userid
1403      * @return void
1404      */
1405     public function unenrol_user(stdClass $instance, $userid) {
1406         global $CFG, $USER, $DB;
1407         require_once("$CFG->dirroot/group/lib.php");
1409         $name = $this->get_name();
1410         $courseid = $instance->courseid;
1412         if ($instance->enrol !== $name) {
1413             throw new coding_exception('invalid enrol instance!');
1414         }
1415         $context = context_course::instance($instance->courseid, MUST_EXIST);
1417         if (!$ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
1418             // weird, user not enrolled
1419             return;
1420         }
1422         // Remove all users groups linked to this enrolment instance.
1423         if ($gms = $DB->get_records('groups_members', array('userid'=>$userid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id))) {
1424             foreach ($gms as $gm) {
1425                 groups_remove_member($gm->groupid, $gm->userid);
1426             }
1427         }
1429         role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id));
1430         $DB->delete_records('user_enrolments', array('id'=>$ue->id));
1432         // add extra info and trigger event
1433         $ue->courseid  = $courseid;
1434         $ue->enrol     = $name;
1436         $sql = "SELECT 'x'
1437                   FROM {user_enrolments} ue
1438                   JOIN {enrol} e ON (e.id = ue.enrolid)
1439                  WHERE ue.userid = :userid AND e.courseid = :courseid";
1440         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
1441             $ue->lastenrol = false;
1443         } else {
1444             // the big cleanup IS necessary!
1445             require_once("$CFG->libdir/gradelib.php");
1447             // remove all remaining roles
1448             role_unassign_all(array('userid'=>$userid, 'contextid'=>$context->id), true, false);
1450             //clean up ALL invisible user data from course if this is the last enrolment - groups, grades, etc.
1451             groups_delete_group_members($courseid, $userid);
1453             grade_user_unenrol($courseid, $userid);
1455             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
1457             $ue->lastenrol = true; // means user not enrolled any more
1458         }
1459         // Trigger event.
1460         $event = \core\event\user_enrolment_deleted::create(
1461                 array(
1462                     'courseid' => $courseid,
1463                     'context' => $context,
1464                     'relateduserid' => $ue->userid,
1465                     'objectid' => $ue->id,
1466                     'other' => array(
1467                         'userenrolment' => (array)$ue,
1468                         'enrol' => $name
1469                         )
1470                     )
1471                 );
1472         $event->trigger();
1473         // reset all enrol caches
1474         $context->mark_dirty();
1476         // reset current user enrolment caching
1477         if ($userid == $USER->id) {
1478             if (isset($USER->enrol['enrolled'][$courseid])) {
1479                 unset($USER->enrol['enrolled'][$courseid]);
1480             }
1481             if (isset($USER->enrol['tempguest'][$courseid])) {
1482                 unset($USER->enrol['tempguest'][$courseid]);
1483                 remove_temp_course_roles($context);
1484             }
1485         }
1486     }
1488     /**
1489      * Forces synchronisation of user enrolments.
1490      *
1491      * This is important especially for external enrol plugins,
1492      * this function is called for all enabled enrol plugins
1493      * right after every user login.
1494      *
1495      * @param object $user user record
1496      * @return void
1497      */
1498     public function sync_user_enrolments($user) {
1499         // override if necessary
1500     }
1502     /**
1503      * Returns link to page which may be used to add new instance of enrolment plugin in course.
1504      * @param int $courseid
1505      * @return moodle_url page url
1506      */
1507     public function get_newinstance_link($courseid) {
1508         // override for most plugins, check if instance already exists in cases only one instance is supported
1509         return NULL;
1510     }
1512     /**
1513      * Is it possible to delete enrol instance via standard UI?
1514      *
1515      * @param object $instance
1516      * @return bool
1517      */
1518     public function instance_deleteable($instance) {
1519         return true;
1520     }
1522     /**
1523      * Returns link to manual enrol UI if exists.
1524      * Does the access control tests automatically.
1525      *
1526      * @param object $instance
1527      * @return moodle_url
1528      */
1529     public function get_manual_enrol_link($instance) {
1530         return NULL;
1531     }
1533     /**
1534      * Returns list of unenrol links for all enrol instances in course.
1535      *
1536      * @param int $instance
1537      * @return moodle_url or NULL if self unenrolment not supported
1538      */
1539     public function get_unenrolself_link($instance) {
1540         global $USER, $CFG, $DB;
1542         $name = $this->get_name();
1543         if ($instance->enrol !== $name) {
1544             throw new coding_exception('invalid enrol instance!');
1545         }
1547         if ($instance->courseid == SITEID) {
1548             return NULL;
1549         }
1551         if (!enrol_is_enabled($name)) {
1552             return NULL;
1553         }
1555         if ($instance->status != ENROL_INSTANCE_ENABLED) {
1556             return NULL;
1557         }
1559         if (!file_exists("$CFG->dirroot/enrol/$name/unenrolself.php")) {
1560             return NULL;
1561         }
1563         $context = context_course::instance($instance->courseid, MUST_EXIST);
1565         if (!has_capability("enrol/$name:unenrolself", $context)) {
1566             return NULL;
1567         }
1569         if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$USER->id, 'status'=>ENROL_USER_ACTIVE))) {
1570             return NULL;
1571         }
1573         return new moodle_url("/enrol/$name/unenrolself.php", array('enrolid'=>$instance->id));
1574     }
1576     /**
1577      * Adds enrol instance UI to course edit form
1578      *
1579      * @param object $instance enrol instance or null if does not exist yet
1580      * @param MoodleQuickForm $mform
1581      * @param object $data
1582      * @param object $context context of existing course or parent category if course does not exist
1583      * @return void
1584      */
1585     public function course_edit_form($instance, MoodleQuickForm $mform, $data, $context) {
1586         // override - usually at least enable/disable switch, has to add own form header
1587     }
1589     /**
1590      * Validates course edit form data
1591      *
1592      * @param object $instance enrol instance or null if does not exist yet
1593      * @param array $data
1594      * @param object $context context of existing course or parent category if course does not exist
1595      * @return array errors array
1596      */
1597     public function course_edit_validation($instance, array $data, $context) {
1598         return array();
1599     }
1601     /**
1602      * Called after updating/inserting course.
1603      *
1604      * @param bool $inserted true if course just inserted
1605      * @param object $course
1606      * @param object $data form data
1607      * @return void
1608      */
1609     public function course_updated($inserted, $course, $data) {
1610         if ($inserted) {
1611             if ($this->get_config('defaultenrol')) {
1612                 $this->add_default_instance($course);
1613             }
1614         }
1615     }
1617     /**
1618      * Add new instance of enrol plugin.
1619      * @param object $course
1620      * @param array instance fields
1621      * @return int id of new instance, null if can not be created
1622      */
1623     public function add_instance($course, array $fields = NULL) {
1624         global $DB;
1626         if ($course->id == SITEID) {
1627             throw new coding_exception('Invalid request to add enrol instance to frontpage.');
1628         }
1630         $instance = new stdClass();
1631         $instance->enrol          = $this->get_name();
1632         $instance->status         = ENROL_INSTANCE_ENABLED;
1633         $instance->courseid       = $course->id;
1634         $instance->enrolstartdate = 0;
1635         $instance->enrolenddate   = 0;
1636         $instance->timemodified   = time();
1637         $instance->timecreated    = $instance->timemodified;
1638         $instance->sortorder      = $DB->get_field('enrol', 'COALESCE(MAX(sortorder), -1) + 1', array('courseid'=>$course->id));
1640         $fields = (array)$fields;
1641         unset($fields['enrol']);
1642         unset($fields['courseid']);
1643         unset($fields['sortorder']);
1644         foreach($fields as $field=>$value) {
1645             $instance->$field = $value;
1646         }
1648         return $DB->insert_record('enrol', $instance);
1649     }
1651     /**
1652      * Add new instance of enrol plugin with default settings,
1653      * called when adding new instance manually or when adding new course.
1654      *
1655      * Not all plugins support this.
1656      *
1657      * @param object $course
1658      * @return int id of new instance or null if no default supported
1659      */
1660     public function add_default_instance($course) {
1661         return null;
1662     }
1664     /**
1665      * Update instance status
1666      *
1667      * Override when plugin needs to do some action when enabled or disabled.
1668      *
1669      * @param stdClass $instance
1670      * @param int $newstatus ENROL_INSTANCE_ENABLED, ENROL_INSTANCE_DISABLED
1671      * @return void
1672      */
1673     public function update_status($instance, $newstatus) {
1674         global $DB;
1676         $instance->status = $newstatus;
1677         $DB->update_record('enrol', $instance);
1679         // invalidate all enrol caches
1680         $context = context_course::instance($instance->courseid);
1681         $context->mark_dirty();
1682     }
1684     /**
1685      * Delete course enrol plugin instance, unenrol all users.
1686      * @param object $instance
1687      * @return void
1688      */
1689     public function delete_instance($instance) {
1690         global $DB;
1692         $name = $this->get_name();
1693         if ($instance->enrol !== $name) {
1694             throw new coding_exception('invalid enrol instance!');
1695         }
1697         //first unenrol all users
1698         $participants = $DB->get_recordset('user_enrolments', array('enrolid'=>$instance->id));
1699         foreach ($participants as $participant) {
1700             $this->unenrol_user($instance, $participant->userid);
1701         }
1702         $participants->close();
1704         // now clean up all remainders that were not removed correctly
1705         $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
1706         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
1707         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
1709         // finally drop the enrol row
1710         $DB->delete_records('enrol', array('id'=>$instance->id));
1712         // invalidate all enrol caches
1713         $context = context_course::instance($instance->courseid);
1714         $context->mark_dirty();
1715     }
1717     /**
1718      * Creates course enrol form, checks if form submitted
1719      * and enrols user if necessary. It can also redirect.
1720      *
1721      * @param stdClass $instance
1722      * @return string html text, usually a form in a text box
1723      */
1724     public function enrol_page_hook(stdClass $instance) {
1725         return null;
1726     }
1728     /**
1729      * Checks if user can self enrol.
1730      *
1731      * @param stdClass $instance enrolment instance
1732      * @param bool $checkuserenrolment if true will check if user enrolment is inactive.
1733      *             used by navigation to improve performance.
1734      * @return bool|string true if successful, else error message or false
1735      */
1736     public function can_self_enrol(stdClass $instance, $checkuserenrolment = true) {
1737         return false;
1738     }
1740     /**
1741      * Return information for enrolment instance containing list of parameters required
1742      * for enrolment, name of enrolment plugin etc.
1743      *
1744      * @param stdClass $instance enrolment instance
1745      * @return array instance info.
1746      */
1747     public function get_enrol_info(stdClass $instance) {
1748         return null;
1749     }
1751     /**
1752      * Adds navigation links into course admin block.
1753      *
1754      * By defaults looks for manage links only.
1755      *
1756      * @param navigation_node $instancesnode
1757      * @param stdClass $instance
1758      * @return void
1759      */
1760     public function add_course_navigation($instancesnode, stdClass $instance) {
1761         // usually adds manage users
1762     }
1764     /**
1765      * Returns edit icons for the page with list of instances
1766      * @param stdClass $instance
1767      * @return array
1768      */
1769     public function get_action_icons(stdClass $instance) {
1770         return array();
1771     }
1773     /**
1774      * Reads version.php and determines if it is necessary
1775      * to execute the cron job now.
1776      * @return bool
1777      */
1778     public function is_cron_required() {
1779         global $CFG;
1781         $name = $this->get_name();
1782         $versionfile = "$CFG->dirroot/enrol/$name/version.php";
1783         $plugin = new stdClass();
1784         include($versionfile);
1785         if (empty($plugin->cron)) {
1786             return false;
1787         }
1788         $lastexecuted = $this->get_config('lastcron', 0);
1789         if ($lastexecuted + $plugin->cron < time()) {
1790             return true;
1791         } else {
1792             return false;
1793         }
1794     }
1796     /**
1797      * Called for all enabled enrol plugins that returned true from is_cron_required().
1798      * @return void
1799      */
1800     public function cron() {
1801     }
1803     /**
1804      * Called when user is about to be deleted
1805      * @param object $user
1806      * @return void
1807      */
1808     public function user_delete($user) {
1809         global $DB;
1811         $sql = "SELECT e.*
1812                   FROM {enrol} e
1813                   JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
1814                  WHERE e.enrol = :name AND ue.userid = :userid";
1815         $params = array('name'=>$this->get_name(), 'userid'=>$user->id);
1817         $rs = $DB->get_recordset_sql($sql, $params);
1818         foreach($rs as $instance) {
1819             $this->unenrol_user($instance, $user->id);
1820         }
1821         $rs->close();
1822     }
1824     /**
1825      * Returns an enrol_user_button that takes the user to a page where they are able to
1826      * enrol users into the managers course through this plugin.
1827      *
1828      * Optional: If the plugin supports manual enrolments it can choose to override this
1829      * otherwise it shouldn't
1830      *
1831      * @param course_enrolment_manager $manager
1832      * @return enrol_user_button|false
1833      */
1834     public function get_manual_enrol_button(course_enrolment_manager $manager) {
1835         return false;
1836     }
1838     /**
1839      * Gets an array of the user enrolment actions
1840      *
1841      * @param course_enrolment_manager $manager
1842      * @param stdClass $ue
1843      * @return array An array of user_enrolment_actions
1844      */
1845     public function get_user_enrolment_actions(course_enrolment_manager $manager, $ue) {
1846         return array();
1847     }
1849     /**
1850      * Returns true if the plugin has one or more bulk operations that can be performed on
1851      * user enrolments.
1852      *
1853      * @param course_enrolment_manager $manager
1854      * @return bool
1855      */
1856     public function has_bulk_operations(course_enrolment_manager $manager) {
1857        return false;
1858     }
1860     /**
1861      * Return an array of enrol_bulk_enrolment_operation objects that define
1862      * the bulk actions that can be performed on user enrolments by the plugin.
1863      *
1864      * @param course_enrolment_manager $manager
1865      * @return array
1866      */
1867     public function get_bulk_operations(course_enrolment_manager $manager) {
1868         return array();
1869     }
1871     /**
1872      * Do any enrolments need expiration processing.
1873      *
1874      * Plugins that want to call this functionality must implement 'expiredaction' config setting.
1875      *
1876      * @param progress_trace $trace
1877      * @param int $courseid one course, empty mean all
1878      * @return bool true if any data processed, false if not
1879      */
1880     public function process_expirations(progress_trace $trace, $courseid = null) {
1881         global $DB;
1883         $name = $this->get_name();
1884         if (!enrol_is_enabled($name)) {
1885             $trace->finished();
1886             return false;
1887         }
1889         $processed = false;
1890         $params = array();
1891         $coursesql = "";
1892         if ($courseid) {
1893             $coursesql = "AND e.courseid = :courseid";
1894         }
1896         // Deal with expired accounts.
1897         $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
1899         if ($action == ENROL_EXT_REMOVED_UNENROL) {
1900             $instances = array();
1901             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
1902                       FROM {user_enrolments} ue
1903                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
1904                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
1905                      WHERE ue.timeend > 0 AND ue.timeend < :now $coursesql";
1906             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'enrol'=>$name, 'courseid'=>$courseid);
1908             $rs = $DB->get_recordset_sql($sql, $params);
1909             foreach ($rs as $ue) {
1910                 if (!$processed) {
1911                     $trace->output("Starting processing of enrol_$name expirations...");
1912                     $processed = true;
1913                 }
1914                 if (empty($instances[$ue->enrolid])) {
1915                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
1916                 }
1917                 $instance = $instances[$ue->enrolid];
1918                 if (!$this->roles_protected()) {
1919                     // Let's just guess what extra roles are supposed to be removed.
1920                     if ($instance->roleid) {
1921                         role_unassign($instance->roleid, $ue->userid, $ue->contextid);
1922                     }
1923                 }
1924                 // The unenrol cleans up all subcontexts if this is the only course enrolment for this user.
1925                 $this->unenrol_user($instance, $ue->userid);
1926                 $trace->output("Unenrolling expired user $ue->userid from course $instance->courseid", 1);
1927             }
1928             $rs->close();
1929             unset($instances);
1931         } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES or $action == ENROL_EXT_REMOVED_SUSPEND) {
1932             $instances = array();
1933             $sql = "SELECT ue.*, e.courseid, c.id AS contextid
1934                       FROM {user_enrolments} ue
1935                       JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :enrol)
1936                       JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
1937                      WHERE ue.timeend > 0 AND ue.timeend < :now
1938                            AND ue.status = :useractive $coursesql";
1939             $params = array('now'=>time(), 'courselevel'=>CONTEXT_COURSE, 'useractive'=>ENROL_USER_ACTIVE, 'enrol'=>$name, 'courseid'=>$courseid);
1940             $rs = $DB->get_recordset_sql($sql, $params);
1941             foreach ($rs as $ue) {
1942                 if (!$processed) {
1943                     $trace->output("Starting processing of enrol_$name expirations...");
1944                     $processed = true;
1945                 }
1946                 if (empty($instances[$ue->enrolid])) {
1947                     $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
1948                 }
1949                 $instance = $instances[$ue->enrolid];
1951                 if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
1952                     if (!$this->roles_protected()) {
1953                         // Let's just guess what roles should be removed.
1954                         $count = $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid));
1955                         if ($count == 1) {
1956                             role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0));
1958                         } else if ($count > 1 and $instance->roleid) {
1959                             role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
1960                         }
1961                     }
1962                     // In any case remove all roles that belong to this instance and user.
1963                     role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'enrol_'.$name, 'itemid'=>$instance->id), true);
1964                     // Final cleanup of subcontexts if there are no more course roles.
1965                     if (0 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
1966                         role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
1967                     }
1968                 }
1970                 $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
1971                 $trace->output("Suspending expired user $ue->userid in course $instance->courseid", 1);
1972             }
1973             $rs->close();
1974             unset($instances);
1976         } else {
1977             // ENROL_EXT_REMOVED_KEEP means no changes.
1978         }
1980         if ($processed) {
1981             $trace->output("...finished processing of enrol_$name expirations");
1982         } else {
1983             $trace->output("No expired enrol_$name enrolments detected");
1984         }
1985         $trace->finished();
1987         return $processed;
1988     }
1990     /**
1991      * Send expiry notifications.
1992      *
1993      * Plugin that wants to have expiry notification MUST implement following:
1994      * - expirynotifyhour plugin setting,
1995      * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold),
1996      * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody,
1997      *   expirymessageenrolledsubject and expirymessageenrolledbody),
1998      * - expiry_notification provider in db/messages.php,
1999      * - upgrade code that sets default thresholds for existing courses (should be 1 day),
2000      * - something that calls this method, such as cron.
2001      *
2002      * @param progress_trace $trace (accepts bool for backwards compatibility only)
2003      */
2004     public function send_expiry_notifications($trace) {
2005         global $DB, $CFG;
2007         $name = $this->get_name();
2008         if (!enrol_is_enabled($name)) {
2009             $trace->finished();
2010             return;
2011         }
2013         // Unfortunately this may take a long time, it should not be interrupted,
2014         // otherwise users get duplicate notification.
2016         @set_time_limit(0);
2017         raise_memory_limit(MEMORY_HUGE);
2020         $expirynotifylast = $this->get_config('expirynotifylast', 0);
2021         $expirynotifyhour = $this->get_config('expirynotifyhour');
2022         if (is_null($expirynotifyhour)) {
2023             debugging("send_expiry_notifications() in $name enrolment plugin needs expirynotifyhour setting");
2024             $trace->finished();
2025             return;
2026         }
2028         if (!($trace instanceof progress_trace)) {
2029             $trace = $trace ? new text_progress_trace() : new null_progress_trace();
2030             debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER);
2031         }
2033         $timenow = time();
2034         $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($expirynotifyhour * 3600);
2036         if ($expirynotifylast > $notifytime) {
2037             $trace->output($name.' enrolment expiry notifications were already sent today at '.userdate($expirynotifylast, '', $CFG->timezone).'.');
2038             $trace->finished();
2039             return;
2041         } else if ($timenow < $notifytime) {
2042             $trace->output($name.' enrolment expiry notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.');
2043             $trace->finished();
2044             return;
2045         }
2047         $trace->output('Processing '.$name.' enrolment expiration notifications...');
2049         // Notify users responsible for enrolment once every day.
2050         $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname
2051                   FROM {user_enrolments} ue
2052                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)
2053                   JOIN {course} c ON (c.id = e.courseid)
2054                   JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)
2055                  WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)
2056               ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC";
2057         $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow, 'name'=>$name);
2059         $rs = $DB->get_recordset_sql($sql, $params);
2061         $lastenrollid = 0;
2062         $users = array();
2064         foreach($rs as $ue) {
2065             if ($lastenrollid and $lastenrollid != $ue->enrolid) {
2066                 $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2067                 $users = array();
2068             }
2069             $lastenrollid = $ue->enrolid;
2071             $enroller = $this->get_enroller($ue->enrolid);
2072             $context = context_course::instance($ue->courseid);
2074             $user = $DB->get_record('user', array('id'=>$ue->userid));
2076             $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend);
2078             if (!$ue->notifyall) {
2079                 continue;
2080             }
2082             if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) {
2083                 // Notify enrolled users only once at the start of the threshold.
2084                 $trace->output("user $ue->userid was already notified that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2085                 continue;
2086             }
2088             $this->notify_expiry_enrolled($user, $ue, $trace);
2089         }
2090         $rs->close();
2092         if ($lastenrollid and $users) {
2093             $this->notify_expiry_enroller($lastenrollid, $users, $trace);
2094         }
2096         $trace->output('...notification processing finished.');
2097         $trace->finished();
2099         $this->set_config('expirynotifylast', $timenow);
2100     }
2102     /**
2103      * Returns the user who is responsible for enrolments for given instance.
2104      *
2105      * Override if plugin knows anybody better than admin.
2106      *
2107      * @param int $instanceid enrolment instance id
2108      * @return stdClass user record
2109      */
2110     protected function get_enroller($instanceid) {
2111         return get_admin();
2112     }
2114     /**
2115      * Notify user about incoming expiration of their enrolment,
2116      * it is called only if notification of enrolled users (aka students) is enabled in course.
2117      *
2118      * This is executed only once for each expiring enrolment right
2119      * at the start of the expiration threshold.
2120      *
2121      * @param stdClass $user
2122      * @param stdClass $ue
2123      * @param progress_trace $trace
2124      */
2125     protected function notify_expiry_enrolled($user, $ue, progress_trace $trace) {
2126         global $CFG, $SESSION;
2128         $name = $this->get_name();
2130         // Some nasty hackery to get strings and dates localised for target user.
2131         $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
2132         if (get_string_manager()->translation_exists($user->lang, false)) {
2133             $SESSION->lang = $user->lang;
2134             moodle_setlocale();
2135         }
2137         $enroller = $this->get_enroller($ue->enrolid);
2138         $context = context_course::instance($ue->courseid);
2140         $a = new stdClass();
2141         $a->course   = format_string($ue->fullname, true, array('context'=>$context));
2142         $a->user     = fullname($user, true);
2143         $a->timeend  = userdate($ue->timeend, '', $user->timezone);
2144         $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user));
2146         $subject = get_string('expirymessageenrolledsubject', 'enrol_'.$name, $a);
2147         $body = get_string('expirymessageenrolledbody', 'enrol_'.$name, $a);
2149         $message = new stdClass();
2150         $message->notification      = 1;
2151         $message->component         = 'enrol_'.$name;
2152         $message->name              = 'expiry_notification';
2153         $message->userfrom          = $enroller;
2154         $message->userto            = $user;
2155         $message->subject           = $subject;
2156         $message->fullmessage       = $body;
2157         $message->fullmessageformat = FORMAT_MARKDOWN;
2158         $message->fullmessagehtml   = markdown_to_html($body);
2159         $message->smallmessage      = $subject;
2160         $message->contexturlname    = $a->course;
2161         $message->contexturl        = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid));
2163         if (message_send($message)) {
2164             $trace->output("notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2165         } else {
2166             $trace->output("error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone), 1);
2167         }
2169         if ($SESSION->lang !== $sessionlang) {
2170             $SESSION->lang = $sessionlang;
2171             moodle_setlocale();
2172         }
2173     }
2175     /**
2176      * Notify person responsible for enrolments that some user enrolments will be expired soon,
2177      * it is called only if notification of enrollers (aka teachers) is enabled in course.
2178      *
2179      * This is called repeatedly every day for each course if there are any pending expiration
2180      * in the expiration threshold.
2181      *
2182      * @param int $eid
2183      * @param array $users
2184      * @param progress_trace $trace
2185      */
2186     protected function notify_expiry_enroller($eid, $users, progress_trace $trace) {
2187         global $DB, $SESSION;
2189         $name = $this->get_name();
2191         $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>$name));
2192         $context = context_course::instance($instance->courseid);
2193         $course = $DB->get_record('course', array('id'=>$instance->courseid));
2195         $enroller = $this->get_enroller($instance->id);
2196         $admin = get_admin();
2198         // Some nasty hackery to get strings and dates localised for target user.
2199         $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null;
2200         if (get_string_manager()->translation_exists($enroller->lang, false)) {
2201             $SESSION->lang = $enroller->lang;
2202             moodle_setlocale();
2203         }
2205         foreach($users as $key=>$info) {
2206             $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone);
2207         }
2209         $a = new stdClass();
2210         $a->course    = format_string($course->fullname, true, array('context'=>$context));
2211         $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24));
2212         $a->users     = implode("\n", $users);
2213         $a->extendurl = (string)new moodle_url('/enrol/users.php', array('id'=>$instance->courseid));
2215         $subject = get_string('expirymessageenrollersubject', 'enrol_'.$name, $a);
2216         $body = get_string('expirymessageenrollerbody', 'enrol_'.$name, $a);
2218         $message = new stdClass();
2219         $message->notification      = 1;
2220         $message->component         = 'enrol_'.$name;
2221         $message->name              = 'expiry_notification';
2222         $message->userfrom          = $admin;
2223         $message->userto            = $enroller;
2224         $message->subject           = $subject;
2225         $message->fullmessage       = $body;
2226         $message->fullmessageformat = FORMAT_MARKDOWN;
2227         $message->fullmessagehtml   = markdown_to_html($body);
2228         $message->smallmessage      = $subject;
2229         $message->contexturlname    = $a->course;
2230         $message->contexturl        = $a->extendurl;
2232         if (message_send($message)) {
2233             $trace->output("notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
2234         } else {
2235             $trace->output("error notifying user $enroller->id about all expiring $name enrolments in course $instance->courseid", 1);
2236         }
2238         if ($SESSION->lang !== $sessionlang) {
2239             $SESSION->lang = $sessionlang;
2240             moodle_setlocale();
2241         }
2242     }
2244     /**
2245      * Automatic enrol sync executed during restore.
2246      * Useful for automatic sync by course->idnumber or course category.
2247      * @param stdClass $course course record
2248      */
2249     public function restore_sync_course($course) {
2250         // Override if necessary.
2251     }
2253     /**
2254      * Restore instance and map settings.
2255      *
2256      * @param restore_enrolments_structure_step $step
2257      * @param stdClass $data
2258      * @param stdClass $course
2259      * @param int $oldid
2260      */
2261     public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
2262         // Do not call this from overridden methods, restore and set new id there.
2263         $step->set_mapping('enrol', $oldid, 0);
2264     }
2266     /**
2267      * Restore user enrolment.
2268      *
2269      * @param restore_enrolments_structure_step $step
2270      * @param stdClass $data
2271      * @param stdClass $instance
2272      * @param int $oldinstancestatus
2273      * @param int $userid
2274      */
2275     public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
2276         // Override as necessary if plugin supports restore of enrolments.
2277     }
2279     /**
2280      * Restore role assignment.
2281      *
2282      * @param stdClass $instance
2283      * @param int $roleid
2284      * @param int $userid
2285      * @param int $contextid
2286      */
2287     public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
2288         // No role assignment by default, override if necessary.
2289     }
2291     /**
2292      * Restore user group membership.
2293      * @param stdClass $instance
2294      * @param int $groupid
2295      * @param int $userid
2296      */
2297     public function restore_group_member($instance, $groupid, $userid) {
2298         // Implement if you want to restore protected group memberships,
2299         // usually this is not necessary because plugins should be able to recreate the memberships automatically.
2300     }